[v9.2] Leaky view and RLS
I'd like to summarize expected issues corresponding to leaky-view and RLS
towards v9.2, and PGcon2011/Developer Meeting.
We already made consensus the leaky-view is a problem to be fixed previous
to the row-level security feature.
We know several ways to leak/infer contents of tuples to be invisible using
a view that is defined to row-level security purpose.
[1]: User defined functions with small-cost and side-effects (E.g error message)
If these functions are appended to WHERE clause, it may be executed earlier
than functions to be performed as row-level security policy of security views.
These function can take arguments that references either visible or invisible
tuples, so functions with side-effects enables to leak the contents of invisible
tuples, when it was invoked earlier than conditions to filter out.
[2]: Iteration in proving PK/FK or UNIQUE constratins
This type of iteration enables to estimate invisible values. E.g, fails to insert
a tuple with a particular primary-key gives us a hint of existence of invisible
tuple. We made consensus that RLS does not handle this type of scenario called as
covert-channels. The point is user cannot see the hidden value directly.
[3]: Reference to statistics delivered from table contents
One example was selectivity-estimator function; Tom mentioned about before.
The pg_statistic holds statistical information delivered from table contents,
so it may help users to infer the contents using its histograms.
The discussion didn't get hot at that time. However, the point of mine is same
as the reason why we don't handle covert-channels.
The statistical is irreversible translation from the original data, so we need
an intelligence process to infer them, not a direct data reference.
We also had a discussion about a principle what type of scenarios should be
considered as problem.
One was that we didn't consider it is not a problem if a function internally
references invisible rows, as long as it consumes them internally. Thus, we
considered index-access-methods can be launched earlier than functions to
filter out violated rows.
Second was that we didn't consider it is not a problem if RLS allows users
to infer invisible rows from the circumstances, as long as it is not possible
to dump invisible data directly, because its bandwidth to leak information
was quite slow. So, we decided not to handle covert-channel such as iteration
of PK/FK proving in RLS.
I still think the scenario [1]User defined functions with small-cost and side-effects (E.g error message) is the only problem to be solved prior to RLS
features. The scenario [2]Iteration in proving PK/FK or UNIQUE constratins and [3]Reference to statistics delivered from table contents don't allow to leak the original data
directly. In addition, the estimator function mentioned in [3]Reference to statistics delivered from table contents just references
statistical data internally. It is same situation with index-access-method.
Please give us the points at issue, if I now overlooked.
Thanks,
--
NEC Europe Ltd, SAP Global Competence Center
KaiGai Kohei <kohei.kaigai@eu.nec.com>
Oops, I overlooked a scenario that we discussed before.
[4]: Unexpected qualifier distributions into inside of join-loop If a qualifier being appended on outside of join-loop references only one-side of the join, the optimizer will unexpectedly distribute it into inside of the join-loop. In the result, these exogenetic qualifiers may be evaluated earlier than security policy functions inside of a view that contains join.
If a qualifier being appended on outside of join-loop references only one-side of the join,
the optimizer will unexpectedly distribute it into inside of the join-loop.
In the result, these exogenetic qualifiers may be evaluated earlier than security policy
functions inside of a view that contains join.
It also allows unprivileged users to reference contents of invisible tuples, so it shall be
a problem to be fixed up according to the principle I described below.
As a reference information, we had discussed the following approach:
We tried to extend CREATE VIEW statement to accept SECURITY attribute to prevent unexpected
qualifier distribution into inside of join-loop originated from security views.
In addition, we also tried to add a variable to track original nest-level of qualifiers to
order qualifiers in WHERE clause. It enabled to fix up [1] and [4]Unexpected qualifier distributions into inside of join-loop If a qualifier being appended on outside of join-loop references only one-side of the join, the optimizer will unexpectedly distribute it into inside of the join-loop. In the result, these exogenetic qualifiers may be evaluated earlier than security policy functions inside of a view that contains join..
However, the [3] was pointed out during the discussion and I was not sure where was the
essence of this issue. So, the patch was returned with feedbacks, then it got postponed
to v9.2 to solve this problem.
Thanks,
-----Original Message-----
From: pgsql-hackers-owner@postgresql.org [mailto:pgsql-hackers-owner@postgresql.org] On Behalf Of
Kohei Kaigai
Sent: 10. Mai 2011 12:56
To: Robert Haas; Stephen Frost; Heikki Linnakangas; Tom Lane
Cc: pgsql-hackers
Subject: [HACKERS] [v9.2] Leaky view and RLSI'd like to summarize expected issues corresponding to leaky-view and RLS
towards v9.2, and PGcon2011/Developer Meeting.We already made consensus the leaky-view is a problem to be fixed previous
to the row-level security feature.We know several ways to leak/infer contents of tuples to be invisible using
a view that is defined to row-level security purpose.[1] User defined functions with small-cost and side-effects (E.g error message)
If these functions are appended to WHERE clause, it may be executed earlier
than functions to be performed as row-level security policy of security views.
These function can take arguments that references either visible or invisible
tuples, so functions with side-effects enables to leak the contents of invisible
tuples, when it was invoked earlier than conditions to filter out.[2] Iteration in proving PK/FK or UNIQUE constratins
This type of iteration enables to estimate invisible values. E.g, fails to insert
a tuple with a particular primary-key gives us a hint of existence of invisible
tuple. We made consensus that RLS does not handle this type of scenario called as
covert-channels. The point is user cannot see the hidden value directly.[3] Reference to statistics delivered from table contents
One example was selectivity-estimator function; Tom mentioned about before.
The pg_statistic holds statistical information delivered from table contents,
so it may help users to infer the contents using its histograms.
The discussion didn't get hot at that time. However, the point of mine is same
as the reason why we don't handle covert-channels.
The statistical is irreversible translation from the original data, so we need
an intelligence process to infer them, not a direct data reference.We also had a discussion about a principle what type of scenarios should be
considered as problem.
One was that we didn't consider it is not a problem if a function internally
references invisible rows, as long as it consumes them internally. Thus, we
considered index-access-methods can be launched earlier than functions to
filter out violated rows.
Second was that we didn't consider it is not a problem if RLS allows users
to infer invisible rows from the circumstances, as long as it is not possible
to dump invisible data directly, because its bandwidth to leak information
was quite slow. So, we decided not to handle covert-channel such as iteration
of PK/FK proving in RLS.I still think the scenario [1] is the only problem to be solved prior to RLS
features. The scenario [2] and [3] don't allow to leak the original data
directly. In addition, the estimator function mentioned in [3] just references
statistical data internally. It is same situation with index-access-method.Please give us the points at issue, if I now overlooked.
--
NEC Europe Ltd, SAP Global Competence Center
KaiGai Kohei <kohei.kaigai@emea.nec.com>
This patch enables to fix up leaky-view problem using functions with tiny cost estimation scenario.
The point of this scenario is criteria to reorder qualifiers of scanning plan in order_qual_clauses(). The optimizer may pull up simple subqueries into upper level, then its qualifier will get merged with ones in the upper level. When executor scans a relation, qualifiers with smaller cost shall be executed earlier to minimize cost to filter out invisible tuples. However, we know unpreferable side-effects when we use a view for row-level security.
Even if a certain subquery rewritten from a view is defined for row-level security, a function with tiny cost appended from outside of the view may executed earlier than qualifiers to perform as security policy of the view, as long as the view is enough simple and the supplied function has tiny cost. In the result, this function can see the arguments come from invisible tuples, and leak them into somewhere.
The solution is quite simple. This patch enables to track original depth of qualifiers and modify criteria to sort qualifiers in order_qual_clauses().
Even if a function with tiny cost is supplied from outside of views, the patched optimizer does not prioritize cost estimation more than the depth.
It fixes up the scenario [1]unexpected reorder of functions with tiny-cost and side-effects in the bellow descriprions.
--------
The background of the leaky-view problem is well summarized at:
http://wiki.postgresql.org/wiki/RLS
We had discussed several scenarios in v9.1 development cycle, and the last developer meeting. We almost concluded the following criteria to characterize whether a leak-view scenario is problematic to be fixed, or not.
* If unprived user can directly reference contents of invisible tuples, it is a problem to be fixed.
* As long as contents of invisible tuples are consumed by internal stuff (eg, index-access method), it is not a problem to be fixed.
Thus, the scenario [1]unexpected reorder of functions with tiny-cost and side-effects and [2]unexpected push-down of functions with side-effect into join-loop are problematic to be fixed, but [3]estimation of hidden value using iteration of PK/FK proves and [4]estimation of hidden value using statistics are not. So, I'll try to fix up these two scenario with the patch part-1 amd part-2.
[1]: unexpected reorder of functions with tiny-cost and side-effects
Qualifiers of WHERE or JOIN ... IN clause shall be sorted by estimated cost, not depth of nest level. Thus, this logic can make order reversal when user-given qualifier has smaller cost than qualifiers to perform as security policy inside of view.
In the result, these qualifiers can reference both of visible and invisible tuples prior to the filtering by row-level security policy of the view. Thus, this behavior can be used to leak contents of invisible tuples.
[2]: unexpected push-down of functions with side-effect into join-loop
If arguments of qualifier being appended on outside of join-loop references only one-side of the join-loop, it is a good strategy to distribute this qualifier into inside of the join-loop to minimize number of tuples to be joined, from the viewpoint of performance.
However, it also makes order reversal when the join-loop is a part of view definition that should perform row-level security policy. Then, these exogenetic qualifiers may be executed prior to the filtering by row-level security policy of the view. Thus, this behavior can be used to leak contents of invisible tuple.
[3]: estimation of hidden value using iteration of PK/FK proves
Due to the nature of PK/FK constraints, we can infer existence of key values being stored within invisible tuple, even if we never allows users to reference contents of invisible tuples.
We commonly call this type of information leaks "covert-channel", and it is basically impossible to prevent according to the previous security research, however, its risk is also relatively small because of slow bandwidth to leak.
We already made consensus this scenario is not a problem to be fixed.
[4]: estimation of hidden value using statistics
One example was selectivity-estimator function; that may reference statistical information delivered from the tables have invisible tuples for optimization. Here are two points to be considered. The one is purely internal stuff may be able to reference invisible tuples, however, it is not a problem as long as it does not leak them into end-users; such as index access methods. The second is statistical or other form of date delivered from invisible tuples. We can set up a table that contains data delivered from invisible tuples using row-level triggers, however, it is quite a matter of database administration. Unless owner of tables set up such a leakable configuration, other users cannot reference them.
Thanks,
--
NEC Europe Ltd, SAP Global Competence Center
KaiGai Kohei <kohei.kaigai@emea.nec.com>
Attachments:
pgsql-fix-leaky-view-part-1.patchapplication/octet-stream; name=pgsql-fix-leaky-view-part-1.patchDownload
src/backend/nodes/copyfuncs.c | 8 ++
src/backend/nodes/equalfuncs.c | 8 ++
src/backend/nodes/outfuncs.c | 8 ++
src/backend/nodes/readfuncs.c | 8 ++
src/backend/optimizer/path/costsize.c | 17 ++++
src/backend/optimizer/plan/createplan.c | 13 +++-
src/backend/optimizer/plan/planner.c | 114 ++++++++++++++++++++++++++++
src/backend/optimizer/util/clauses.c | 24 ++++--
src/include/nodes/primnodes.h | 6 ++
src/include/nodes/relation.h | 1 +
src/test/regress/expected/select_views.out | 75 +++++++++++++++----
src/test/regress/sql/select_views.sql | 43 +++++++++++
12 files changed, 302 insertions(+), 23 deletions(-)
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index c9133dd..1b1cf93 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -1188,6 +1188,7 @@ _copyFuncExpr(FuncExpr *from)
COPY_SCALAR_FIELD(inputcollid);
COPY_NODE_FIELD(args);
COPY_LOCATION_FIELD(location);
+ COPY_SCALAR_FIELD(depth);
return newnode;
}
@@ -1224,6 +1225,7 @@ _copyOpExpr(OpExpr *from)
COPY_SCALAR_FIELD(inputcollid);
COPY_NODE_FIELD(args);
COPY_LOCATION_FIELD(location);
+ COPY_SCALAR_FIELD(depth);
return newnode;
}
@@ -1244,6 +1246,7 @@ _copyDistinctExpr(DistinctExpr *from)
COPY_SCALAR_FIELD(inputcollid);
COPY_NODE_FIELD(args);
COPY_LOCATION_FIELD(location);
+ COPY_SCALAR_FIELD(depth);
return newnode;
}
@@ -1264,6 +1267,7 @@ _copyNullIfExpr(NullIfExpr *from)
COPY_SCALAR_FIELD(inputcollid);
COPY_NODE_FIELD(args);
COPY_LOCATION_FIELD(location);
+ COPY_SCALAR_FIELD(depth);
return newnode;
}
@@ -1282,6 +1286,7 @@ _copyScalarArrayOpExpr(ScalarArrayOpExpr *from)
COPY_SCALAR_FIELD(inputcollid);
COPY_NODE_FIELD(args);
COPY_LOCATION_FIELD(location);
+ COPY_SCALAR_FIELD(depth);
return newnode;
}
@@ -1422,6 +1427,7 @@ _copyCoerceViaIO(CoerceViaIO *from)
COPY_SCALAR_FIELD(resultcollid);
COPY_SCALAR_FIELD(coerceformat);
COPY_LOCATION_FIELD(location);
+ COPY_SCALAR_FIELD(depth);
return newnode;
}
@@ -1442,6 +1448,7 @@ _copyArrayCoerceExpr(ArrayCoerceExpr *from)
COPY_SCALAR_FIELD(isExplicit);
COPY_SCALAR_FIELD(coerceformat);
COPY_LOCATION_FIELD(location);
+ COPY_SCALAR_FIELD(depth);
return newnode;
}
@@ -1574,6 +1581,7 @@ _copyRowCompareExpr(RowCompareExpr *from)
COPY_NODE_FIELD(inputcollids);
COPY_NODE_FIELD(largs);
COPY_NODE_FIELD(rargs);
+ COPY_SCALAR_FIELD(depth);
return newnode;
}
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 3a0267c..71c4694 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -248,6 +248,7 @@ _equalFuncExpr(FuncExpr *a, FuncExpr *b)
COMPARE_SCALAR_FIELD(inputcollid);
COMPARE_NODE_FIELD(args);
COMPARE_LOCATION_FIELD(location);
+ COMPARE_SCALAR_FIELD(depth);
return true;
}
@@ -285,6 +286,7 @@ _equalOpExpr(OpExpr *a, OpExpr *b)
COMPARE_SCALAR_FIELD(inputcollid);
COMPARE_NODE_FIELD(args);
COMPARE_LOCATION_FIELD(location);
+ COMPARE_SCALAR_FIELD(depth);
return true;
}
@@ -311,6 +313,7 @@ _equalDistinctExpr(DistinctExpr *a, DistinctExpr *b)
COMPARE_SCALAR_FIELD(inputcollid);
COMPARE_NODE_FIELD(args);
COMPARE_LOCATION_FIELD(location);
+ COMPARE_SCALAR_FIELD(depth);
return true;
}
@@ -337,6 +340,7 @@ _equalNullIfExpr(NullIfExpr *a, NullIfExpr *b)
COMPARE_SCALAR_FIELD(inputcollid);
COMPARE_NODE_FIELD(args);
COMPARE_LOCATION_FIELD(location);
+ COMPARE_SCALAR_FIELD(depth);
return true;
}
@@ -361,6 +365,7 @@ _equalScalarArrayOpExpr(ScalarArrayOpExpr *a, ScalarArrayOpExpr *b)
COMPARE_SCALAR_FIELD(inputcollid);
COMPARE_NODE_FIELD(args);
COMPARE_LOCATION_FIELD(location);
+ COMPARE_SCALAR_FIELD(depth);
return true;
}
@@ -479,6 +484,7 @@ _equalCoerceViaIO(CoerceViaIO *a, CoerceViaIO *b)
return false;
COMPARE_LOCATION_FIELD(location);
+ COMPARE_SCALAR_FIELD(depth);
return true;
}
@@ -503,6 +509,7 @@ _equalArrayCoerceExpr(ArrayCoerceExpr *a, ArrayCoerceExpr *b)
return false;
COMPARE_LOCATION_FIELD(location);
+ COMPARE_SCALAR_FIELD(depth);
return true;
}
@@ -613,6 +620,7 @@ _equalRowCompareExpr(RowCompareExpr *a, RowCompareExpr *b)
COMPARE_NODE_FIELD(inputcollids);
COMPARE_NODE_FIELD(largs);
COMPARE_NODE_FIELD(rargs);
+ COMPARE_SCALAR_FIELD(depth);
return true;
}
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 681f5f8..f9a8aef 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -1004,6 +1004,7 @@ _outFuncExpr(StringInfo str, FuncExpr *node)
WRITE_OID_FIELD(inputcollid);
WRITE_NODE_FIELD(args);
WRITE_LOCATION_FIELD(location);
+ WRITE_INT_FIELD(depth);
}
static void
@@ -1030,6 +1031,7 @@ _outOpExpr(StringInfo str, OpExpr *node)
WRITE_OID_FIELD(inputcollid);
WRITE_NODE_FIELD(args);
WRITE_LOCATION_FIELD(location);
+ WRITE_INT_FIELD(depth);
}
static void
@@ -1045,6 +1047,7 @@ _outDistinctExpr(StringInfo str, DistinctExpr *node)
WRITE_OID_FIELD(inputcollid);
WRITE_NODE_FIELD(args);
WRITE_LOCATION_FIELD(location);
+ WRITE_INT_FIELD(depth);
}
static void
@@ -1060,6 +1063,7 @@ _outNullIfExpr(StringInfo str, NullIfExpr *node)
WRITE_OID_FIELD(inputcollid);
WRITE_NODE_FIELD(args);
WRITE_LOCATION_FIELD(location);
+ WRITE_INT_FIELD(depth);
}
static void
@@ -1073,6 +1077,7 @@ _outScalarArrayOpExpr(StringInfo str, ScalarArrayOpExpr *node)
WRITE_OID_FIELD(inputcollid);
WRITE_NODE_FIELD(args);
WRITE_LOCATION_FIELD(location);
+ WRITE_INT_FIELD(depth);
}
static void
@@ -1190,6 +1195,7 @@ _outCoerceViaIO(StringInfo str, CoerceViaIO *node)
WRITE_OID_FIELD(resultcollid);
WRITE_ENUM_FIELD(coerceformat, CoercionForm);
WRITE_LOCATION_FIELD(location);
+ WRITE_INT_FIELD(depth);
}
static void
@@ -1205,6 +1211,7 @@ _outArrayCoerceExpr(StringInfo str, ArrayCoerceExpr *node)
WRITE_BOOL_FIELD(isExplicit);
WRITE_ENUM_FIELD(coerceformat, CoercionForm);
WRITE_LOCATION_FIELD(location);
+ WRITE_INT_FIELD(depth);
}
static void
@@ -1297,6 +1304,7 @@ _outRowCompareExpr(StringInfo str, RowCompareExpr *node)
WRITE_NODE_FIELD(inputcollids);
WRITE_NODE_FIELD(largs);
WRITE_NODE_FIELD(rargs);
+ WRITE_INT_FIELD(depth);
}
static void
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 2288514..fdd7f1f 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -540,6 +540,7 @@ _readFuncExpr(void)
READ_OID_FIELD(inputcollid);
READ_NODE_FIELD(args);
READ_LOCATION_FIELD(location);
+ READ_INT_FIELD(depth);
READ_DONE();
}
@@ -587,6 +588,7 @@ _readOpExpr(void)
READ_OID_FIELD(inputcollid);
READ_NODE_FIELD(args);
READ_LOCATION_FIELD(location);
+ READ_INT_FIELD(depth);
READ_DONE();
}
@@ -618,6 +620,7 @@ _readDistinctExpr(void)
READ_OID_FIELD(inputcollid);
READ_NODE_FIELD(args);
READ_LOCATION_FIELD(location);
+ READ_INT_FIELD(depth);
READ_DONE();
}
@@ -649,6 +652,7 @@ _readNullIfExpr(void)
READ_OID_FIELD(inputcollid);
READ_NODE_FIELD(args);
READ_LOCATION_FIELD(location);
+ READ_INT_FIELD(depth);
READ_DONE();
}
@@ -678,6 +682,7 @@ _readScalarArrayOpExpr(void)
READ_OID_FIELD(inputcollid);
READ_NODE_FIELD(args);
READ_LOCATION_FIELD(location);
+ READ_INT_FIELD(depth);
READ_DONE();
}
@@ -793,6 +798,7 @@ _readCoerceViaIO(void)
READ_OID_FIELD(resultcollid);
READ_ENUM_FIELD(coerceformat, CoercionForm);
READ_LOCATION_FIELD(location);
+ READ_INT_FIELD(depth);
READ_DONE();
}
@@ -813,6 +819,7 @@ _readArrayCoerceExpr(void)
READ_BOOL_FIELD(isExplicit);
READ_ENUM_FIELD(coerceformat, CoercionForm);
READ_LOCATION_FIELD(location);
+ READ_INT_FIELD(depth);
READ_DONE();
}
@@ -945,6 +952,7 @@ _readRowCompareExpr(void)
READ_NODE_FIELD(inputcollids);
READ_NODE_FIELD(largs);
READ_NODE_FIELD(rargs);
+ READ_INT_FIELD(depth);
READ_DONE();
}
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index c4404b1..11fbe61 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -2593,6 +2593,7 @@ cost_qual_eval(QualCost *cost, List *quals, PlannerInfo *root)
context.root = root;
context.total.startup = 0;
context.total.per_tuple = 0;
+ context.total.depth = 0;
/* We don't charge any cost for the implicit ANDing at top level ... */
@@ -2618,6 +2619,7 @@ cost_qual_eval_node(QualCost *cost, Node *qual, PlannerInfo *root)
context.root = root;
context.total.startup = 0;
context.total.per_tuple = 0;
+ context.total.depth = 0;
cost_qual_eval_walker(qual, &context);
@@ -2647,6 +2649,7 @@ cost_qual_eval_walker(Node *node, cost_qual_eval_context *context)
locContext.root = context->root;
locContext.total.startup = 0;
locContext.total.per_tuple = 0;
+ locContext.total.depth = 0;
/*
* For an OR clause, recurse into the marked-up tree so that we
@@ -2671,6 +2674,8 @@ cost_qual_eval_walker(Node *node, cost_qual_eval_context *context)
}
context->total.startup += rinfo->eval_cost.startup;
context->total.per_tuple += rinfo->eval_cost.per_tuple;
+ if (rinfo->eval_cost.depth > context->total.depth)
+ context->total.depth = rinfo->eval_cost.depth;
/* do NOT recurse into children */
return false;
}
@@ -2694,6 +2699,8 @@ cost_qual_eval_walker(Node *node, cost_qual_eval_context *context)
{
context->total.per_tuple +=
get_func_cost(((FuncExpr *) node)->funcid) * cpu_operator_cost;
+ if (((FuncExpr *)node)->depth > context->total.depth)
+ context->total.depth = ((FuncExpr *)node)->depth;
}
else if (IsA(node, OpExpr) ||
IsA(node, DistinctExpr) ||
@@ -2703,6 +2710,8 @@ cost_qual_eval_walker(Node *node, cost_qual_eval_context *context)
set_opfuncid((OpExpr *) node);
context->total.per_tuple +=
get_func_cost(((OpExpr *) node)->opfuncid) * cpu_operator_cost;
+ if (((OpExpr *)node)->depth > context->total.depth)
+ context->total.depth = ((OpExpr *)node)->depth;
}
else if (IsA(node, ScalarArrayOpExpr))
{
@@ -2716,6 +2725,8 @@ cost_qual_eval_walker(Node *node, cost_qual_eval_context *context)
set_sa_opfuncid(saop);
context->total.per_tuple += get_func_cost(saop->opfuncid) *
cpu_operator_cost * estimate_array_length(arraynode) * 0.5;
+ if (saop->depth > context->total.depth)
+ context->total.depth = saop->depth;
}
else if (IsA(node, Aggref) ||
IsA(node, WindowFunc))
@@ -2746,6 +2757,8 @@ cost_qual_eval_walker(Node *node, cost_qual_eval_context *context)
getTypeOutputInfo(exprType((Node *) iocoerce->arg),
&iofunc, &typisvarlena);
context->total.per_tuple += get_func_cost(iofunc) * cpu_operator_cost;
+ if (iocoerce->depth > context->total.depth)
+ context->total.depth = iocoerce->depth;
}
else if (IsA(node, ArrayCoerceExpr))
{
@@ -2755,6 +2768,8 @@ cost_qual_eval_walker(Node *node, cost_qual_eval_context *context)
if (OidIsValid(acoerce->elemfuncid))
context->total.per_tuple += get_func_cost(acoerce->elemfuncid) *
cpu_operator_cost * estimate_array_length(arraynode);
+ if (acoerce->depth > context->total.depth)
+ context->total.depth = acoerce->depth;
}
else if (IsA(node, RowCompareExpr))
{
@@ -2769,6 +2784,8 @@ cost_qual_eval_walker(Node *node, cost_qual_eval_context *context)
context->total.per_tuple += get_func_cost(get_opcode(opid)) *
cpu_operator_cost;
}
+ if (rcexpr->depth > context->total.depth)
+ context->total.depth = rcexpr->depth;
}
else if (IsA(node, CurrentOfExpr))
{
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index ac80511..829127a 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -2704,6 +2704,7 @@ order_qual_clauses(PlannerInfo *root, List *clauses)
{
Node *clause;
Cost cost;
+ int depth;
} QualItem;
int nitems = list_length(clauses);
QualItem *items;
@@ -2729,6 +2730,7 @@ order_qual_clauses(PlannerInfo *root, List *clauses)
cost_qual_eval_node(&qcost, clause, root);
items[i].clause = clause;
items[i].cost = qcost.per_tuple;
+ items[i].depth = qcost.depth;
i++;
}
@@ -2745,7 +2747,16 @@ order_qual_clauses(PlannerInfo *root, List *clauses)
/* insert newitem into the already-sorted subarray */
for (j = i; j > 0; j--)
{
- if (newitem.cost >= items[j - 1].cost)
+ /*
+ * Higher priority shall be given to the items originated from
+ * deeper nest level. If same level, it shall be given to the
+ * items with smaller estimated cost.
+ * Such kind of consideration is needed to prevent leaky-view
+ * problem.
+ */
+ if (newitem.depth < items[j - 1].depth ||
+ (newitem.depth == items[j - 1].depth &&
+ newitem.cost >= items[j - 1].cost))
break;
items[j] = items[j - 1];
}
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 7b2b40f..70c9524 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -33,6 +33,7 @@
#include "optimizer/subselect.h"
#include "optimizer/tlist.h"
#include "optimizer/var.h"
+#include "nodes/nodeFuncs.h"
#ifdef OPTIMIZER_DEBUG
#include "nodes/print.h"
#endif
@@ -103,6 +104,7 @@ static void get_column_info_for_window(PlannerInfo *root, WindowClause *wc,
int *ordNumCols,
AttrNumber **ordColIdx,
Oid **ordOperators);
+static void mark_qualifiers_depth(Query *query);
/*****************************************************************************
@@ -148,6 +150,12 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams)
cursorOptions |= ((DeclareCursorStmt *) parse->utilityStmt)->options;
/*
+ * Mark qualifiers its original depth to prevent reversal of orders
+ * on evaluation of WHERE clause during relation scanns.
+ */
+ mark_qualifiers_depth(parse);
+
+ /*
* Set up global state for this planner invocation. This data is needed
* across all levels of sub-Query that might exist in the given command,
* so we keep it in a separate struct that's linked to by each per-Query
@@ -2993,6 +3001,112 @@ get_column_info_for_window(PlannerInfo *root, WindowClause *wc, List *tlist,
}
}
+/*
+ * mark_qualifiers_depth
+ *
+ * It marks depth field of the each expression nodes that eventually
+ * invokes functions, to track the original nest-level. On the evaluation
+ * of qualifiers within WHERE or JOIN ... ON clauses during relation scans,
+ * these items shall be reordered according to the nest-level and estimated
+ * cost.
+ * The optimizer may pull-up simple sub-queries or join clause, and
+ * qualifiers to filter out tuples shall be mixed with ones in upper-
+ * level. Thus, we need to track the original nest-level of qualifiers
+ * to prevent reverse of order in evaluation, because some of qualifiers
+ * can have side-effects that allows to leak supplied argument to outside.
+ * It can be abused to break row-level security using a user defined function
+ * with very small estimated cost, so nest level of qualifiers originated
+ * from is used as a criteria, rather than estimated cost, to decide order
+ * to evaluate qualifiers.
+ */
+static bool
+mark_qualifiers_depth_walker(Node *node, void *context)
+{
+ int depth = *((int *)(context));
+
+ if (node == NULL)
+ return false;
+ if (IsA(node, FuncExpr))
+ {
+ FuncExpr *exp = (FuncExpr *)node;
+
+ exp->depth = depth;
+
+ return false;
+ }
+ else if (IsA(node, OpExpr))
+ {
+ OpExpr *exp = (OpExpr *)node;
+
+ exp->depth = depth;
+
+ return false;
+ }
+ else if (IsA(node, DistinctExpr))
+ {
+ DistinctExpr *exp = (DistinctExpr *)node;
+
+ exp->depth = depth;
+
+ return false;
+ }
+ else if (IsA(node, ScalarArrayOpExpr))
+ {
+ ScalarArrayOpExpr *exp = (ScalarArrayOpExpr *)node;
+
+ exp->depth = depth;
+
+ return false;
+ }
+ else if (IsA(node, CoerceViaIO))
+ {
+ CoerceViaIO *exp = (CoerceViaIO *)node;
+
+ exp->depth = depth;
+
+ return false;
+ }
+ else if (IsA(node, ArrayCoerceExpr))
+ {
+ ArrayCoerceExpr *exp = (ArrayCoerceExpr *)node;
+
+ exp->depth = depth;
+
+ return false;
+ }
+ else if (IsA(node, NullIfExpr))
+ {
+ NullIfExpr *exp = (NullIfExpr *)node;
+
+ exp->depth = depth;
+
+ return false;
+ }
+ else if (IsA(node, RowCompareExpr))
+ {
+ RowCompareExpr *exp = (RowCompareExpr *)node;
+
+ exp->depth = depth;
+
+ return false;
+ }
+ else if (IsA(node, Query))
+ {
+ depth += 2;
+
+ query_tree_walker((Query *)node, mark_qualifiers_depth_walker, &depth, 0);
+ return false;
+ }
+ return expression_tree_walker(node, mark_qualifiers_depth_walker, context);
+}
+
+static void
+mark_qualifiers_depth(Query *query)
+{
+ int depth = 0;
+
+ query_tree_walker(query, mark_qualifiers_depth_walker, &depth, 0);
+}
/*
* expression_planner
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index 8b0d862..b6373b7 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -111,6 +111,7 @@ static Expr *simplify_function(Oid funcid,
Oid result_collid, Oid input_collid, List **args,
bool has_named_args,
bool allow_inline,
+ int depth,
eval_const_expressions_context *context);
static List *reorder_function_arguments(List *args, Oid result_type,
HeapTuple func_tuple,
@@ -123,7 +124,7 @@ static void recheck_cast_function_args(List *args, Oid result_type,
HeapTuple func_tuple);
static Expr *evaluate_function(Oid funcid, Oid result_type, int32 result_typmod,
Oid result_collid, Oid input_collid, List *args,
- HeapTuple func_tuple,
+ HeapTuple func_tuple, int depth,
eval_const_expressions_context *context);
static Expr *inline_function(Oid funcid, Oid result_type, Oid result_collid,
Oid input_collid, List *args,
@@ -2228,7 +2229,7 @@ eval_const_expressions_mutator(Node *node,
expr->funccollid,
expr->inputcollid,
&args,
- has_named_args, true, context);
+ has_named_args, true, expr->depth, context);
if (simple) /* successfully simplified it */
return (Node *) simple;
@@ -2247,6 +2248,7 @@ eval_const_expressions_mutator(Node *node,
newexpr->inputcollid = expr->inputcollid;
newexpr->args = args;
newexpr->location = expr->location;
+ newexpr->depth = expr->depth;
return (Node *) newexpr;
}
if (IsA(node, OpExpr))
@@ -2280,7 +2282,7 @@ eval_const_expressions_mutator(Node *node,
expr->opcollid,
expr->inputcollid,
&args,
- false, true, context);
+ false, true, expr->depth, context);
if (simple) /* successfully simplified it */
return (Node *) simple;
@@ -2311,6 +2313,7 @@ eval_const_expressions_mutator(Node *node,
newexpr->inputcollid = expr->inputcollid;
newexpr->args = args;
newexpr->location = expr->location;
+ newexpr->depth = expr->depth;
return (Node *) newexpr;
}
if (IsA(node, DistinctExpr))
@@ -2377,7 +2380,7 @@ eval_const_expressions_mutator(Node *node,
expr->opcollid,
expr->inputcollid,
&args,
- false, false, context);
+ false, false, expr->depth, context);
if (simple) /* successfully simplified it */
{
/*
@@ -2407,6 +2410,7 @@ eval_const_expressions_mutator(Node *node,
newexpr->inputcollid = expr->inputcollid;
newexpr->args = args;
newexpr->location = expr->location;
+ newexpr->depth = expr->depth;
return (Node *) newexpr;
}
if (IsA(node, BoolExpr))
@@ -2566,7 +2570,7 @@ eval_const_expressions_mutator(Node *node,
InvalidOid,
InvalidOid,
&args,
- false, true, context);
+ false, true, expr->depth, context);
if (simple) /* successfully simplified output fn */
{
/*
@@ -2586,7 +2590,7 @@ eval_const_expressions_mutator(Node *node,
expr->resultcollid,
InvalidOid,
&args,
- false, true, context);
+ false, true, expr->depth, context);
if (simple) /* successfully simplified input fn */
return (Node *) simple;
}
@@ -2602,6 +2606,7 @@ eval_const_expressions_mutator(Node *node,
newexpr->resultcollid = expr->resultcollid;
newexpr->coerceformat = expr->coerceformat;
newexpr->location = expr->location;
+ newexpr->depth = expr->depth;
return (Node *) newexpr;
}
if (IsA(node, ArrayCoerceExpr))
@@ -2626,6 +2631,7 @@ eval_const_expressions_mutator(Node *node,
newexpr->isExplicit = expr->isExplicit;
newexpr->coerceformat = expr->coerceformat;
newexpr->location = expr->location;
+ newexpr->depth = expr->depth;
/*
* If constant argument and it's a binary-coercible or immutable
@@ -3437,6 +3443,7 @@ simplify_function(Oid funcid, Oid result_type, int32 result_typmod,
Oid result_collid, Oid input_collid, List **args,
bool has_named_args,
bool allow_inline,
+ int depth,
eval_const_expressions_context *context)
{
HeapTuple func_tuple;
@@ -3466,7 +3473,7 @@ simplify_function(Oid funcid, Oid result_type, int32 result_typmod,
newexpr = evaluate_function(funcid, result_type, result_typmod,
result_collid, input_collid, *args,
- func_tuple, context);
+ func_tuple, depth, context);
if (!newexpr && allow_inline)
newexpr = inline_function(funcid, result_type, result_collid,
@@ -3719,7 +3726,7 @@ recheck_cast_function_args(List *args, Oid result_type, HeapTuple func_tuple)
static Expr *
evaluate_function(Oid funcid, Oid result_type, int32 result_typmod,
Oid result_collid, Oid input_collid, List *args,
- HeapTuple func_tuple,
+ HeapTuple func_tuple, int depth,
eval_const_expressions_context *context)
{
Form_pg_proc funcform = (Form_pg_proc) GETSTRUCT(func_tuple);
@@ -3805,6 +3812,7 @@ evaluate_function(Oid funcid, Oid result_type, int32 result_typmod,
newexpr->inputcollid = input_collid;
newexpr->args = args;
newexpr->location = -1;
+ newexpr->depth = depth;
return evaluate_expr((Expr *) newexpr, result_type, result_typmod,
result_collid);
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index f1e20ef..b626386 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -335,6 +335,7 @@ typedef struct FuncExpr
Oid inputcollid; /* OID of collation that function should use */
List *args; /* arguments to the function */
int location; /* token location, or -1 if unknown */
+ int depth; /* depth of clause in the original query */
} FuncExpr;
/*
@@ -380,6 +381,7 @@ typedef struct OpExpr
Oid inputcollid; /* OID of collation that operator should use */
List *args; /* arguments to the operator (1 or 2) */
int location; /* token location, or -1 if unknown */
+ int depth; /* depth of clause in the original query */
} OpExpr;
/*
@@ -421,6 +423,7 @@ typedef struct ScalarArrayOpExpr
Oid inputcollid; /* OID of collation that operator should use */
List *args; /* the scalar and array operands */
int location; /* token location, or -1 if unknown */
+ int depth; /* depth of clause in the original query */
} ScalarArrayOpExpr;
/*
@@ -685,6 +688,7 @@ typedef struct CoerceViaIO
Oid resultcollid; /* OID of collation, or InvalidOid if none */
CoercionForm coerceformat; /* how to display this node */
int location; /* token location, or -1 if unknown */
+ int depth; /* depth of clause in the original query */
} CoerceViaIO;
/* ----------------
@@ -710,6 +714,7 @@ typedef struct ArrayCoerceExpr
bool isExplicit; /* conversion semantics flag to pass to func */
CoercionForm coerceformat; /* how to display this node */
int location; /* token location, or -1 if unknown */
+ int depth; /* depth of clause in the original query */
} ArrayCoerceExpr;
/* ----------------
@@ -901,6 +906,7 @@ typedef struct RowCompareExpr
List *inputcollids; /* OID list of collations for comparisons */
List *largs; /* the left-hand input arguments */
List *rargs; /* the right-hand input arguments */
+ int depth; /* depth of clause in the original query */
} RowCompareExpr;
/*
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index f659269..250191b 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -44,6 +44,7 @@ typedef struct QualCost
{
Cost startup; /* one-time cost */
Cost per_tuple; /* per-evaluation cost */
+ int depth; /* depth of qual in the original query */
} QualCost;
/*
diff --git a/src/test/regress/expected/select_views.out b/src/test/regress/expected/select_views.out
index 6cd317c..8b9b83a 100644
--- a/src/test/regress/expected/select_views.out
+++ b/src/test/regress/expected/select_views.out
@@ -467,6 +467,20 @@ SELECT name, #thepath FROM iexit ORDER BY 1, 2;
I- 580 | 21
I- 580 | 22
I- 580 | 22
+ I- 580/I-680 Ramp | 2
+ I- 580/I-680 Ramp | 2
+ I- 580/I-680 Ramp | 2
+ I- 580/I-680 Ramp | 2
+ I- 580/I-680 Ramp | 2
+ I- 580/I-680 Ramp | 2
+ I- 580/I-680 Ramp | 4
+ I- 580/I-680 Ramp | 4
+ I- 580/I-680 Ramp | 4
+ I- 580/I-680 Ramp | 4
+ I- 580/I-680 Ramp | 5
+ I- 580/I-680 Ramp | 6
+ I- 580/I-680 Ramp | 6
+ I- 580/I-680 Ramp | 6
I- 580 Ramp | 2
I- 580 Ramp | 2
I- 580 Ramp | 2
@@ -717,20 +731,6 @@ SELECT name, #thepath FROM iexit ORDER BY 1, 2;
I- 580 Ramp | 8
I- 580 Ramp | 8
I- 580 Ramp | 8
- I- 580/I-680 Ramp | 2
- I- 580/I-680 Ramp | 2
- I- 580/I-680 Ramp | 2
- I- 580/I-680 Ramp | 2
- I- 580/I-680 Ramp | 2
- I- 580/I-680 Ramp | 2
- I- 580/I-680 Ramp | 4
- I- 580/I-680 Ramp | 4
- I- 580/I-680 Ramp | 4
- I- 580/I-680 Ramp | 4
- I- 580/I-680 Ramp | 5
- I- 580/I-680 Ramp | 6
- I- 580/I-680 Ramp | 6
- I- 580/I-680 Ramp | 6
I- 680 | 2
I- 680 | 2
I- 680 | 2
@@ -1247,3 +1247,50 @@ SELECT * FROM toyemp WHERE name = 'sharon';
sharon | 25 | (15,12) | 12000
(1 row)
+--
+-- Test for leaky-view problem
+--
+-- setups
+SET client_min_messages TO 'warning';
+DROP ROLE IF EXISTS alice;
+DROP FUNCTION IF EXISTS f_leak(text);
+DROP TABLE IF EXISTS credit_cards;
+RESET client_min_messages;
+CREATE USER alice;
+CREATE FUNCTION f_leak(text, text)
+ RETURNS bool LANGUAGE 'plpgsql'
+ AS 'begin raise notice ''% => %'', $1, $2; return true; end';
+CREATE TABLE credit_cards (
+ name text,
+ number text,
+ expired text
+);
+INSERT INTO credit_cards VALUES ('alice', '1111-2222-3333-4444', 'Aug-2012'),
+ ('bob', '5555-6666-7777-8888', 'Nov-2016'),
+ ('eve', '9801-2345-6789-0123', 'Jan-2018');
+CREATE VIEW your_credit AS
+ SELECT * FROM credit_cards WHERE name = getpgusername();
+GRANT SELECT ON your_credit TO public;
+-- run leaky view
+SET SESSION AUTHORIZATION alice;
+SELECT * FROM your_credit WHERE f_leak(number,expired);
+NOTICE: 1111-2222-3333-4444 => Aug-2012
+ name | number | expired
+-------+---------------------+----------
+ alice | 1111-2222-3333-4444 | Aug-2012
+(1 row)
+
+EXPLAIN SELECT * FROM your_credit WHERE f_leak(number,expired);
+ QUERY PLAN
+--------------------------------------------------------------------------
+ Seq Scan on credit_cards (cost=0.00..181.20 rows=1 width=96)
+ Filter: ((name = (getpgusername())::text) AND f_leak(number, expired))
+(2 rows)
+
+\c -
+-- cleanups
+DROP ROLE IF EXISTS alice;
+DROP FUNCTION IF EXISTS f_leak(text);
+NOTICE: function f_leak(text) does not exist, skipping
+DROP TABLE IF EXISTS credit_cards CASCADE;
+NOTICE: drop cascades to view your_credit
diff --git a/src/test/regress/sql/select_views.sql b/src/test/regress/sql/select_views.sql
index 14f1be8..f530a7e 100644
--- a/src/test/regress/sql/select_views.sql
+++ b/src/test/regress/sql/select_views.sql
@@ -8,3 +8,46 @@ SELECT * FROM street;
SELECT name, #thepath FROM iexit ORDER BY 1, 2;
SELECT * FROM toyemp WHERE name = 'sharon';
+
+--
+-- Test for leaky-view problem
+--
+
+-- setups
+SET client_min_messages TO 'warning';
+
+DROP ROLE IF EXISTS alice;
+DROP FUNCTION IF EXISTS f_leak(text);
+DROP TABLE IF EXISTS credit_cards;
+
+RESET client_min_messages;
+
+CREATE USER alice;
+CREATE FUNCTION f_leak(text, text)
+ RETURNS bool LANGUAGE 'plpgsql'
+ AS 'begin raise notice ''% => %'', $1, $2; return true; end';
+CREATE TABLE credit_cards (
+ name text,
+ number text,
+ expired text
+);
+
+INSERT INTO credit_cards VALUES ('alice', '1111-2222-3333-4444', 'Aug-2012'),
+ ('bob', '5555-6666-7777-8888', 'Nov-2016'),
+ ('eve', '9801-2345-6789-0123', 'Jan-2018');
+CREATE VIEW your_credit AS
+ SELECT * FROM credit_cards WHERE name = getpgusername();
+
+GRANT SELECT ON your_credit TO public;
+-- run leaky view
+SET SESSION AUTHORIZATION alice;
+
+SELECT * FROM your_credit WHERE f_leak(number,expired);
+EXPLAIN SELECT * FROM your_credit WHERE f_leak(number,expired);
+
+\c -
+-- cleanups
+DROP ROLE IF EXISTS alice;
+
+DROP FUNCTION IF EXISTS f_leak(text);
+DROP TABLE IF EXISTS credit_cards CASCADE;
This patch enables to fix up leaky-view problem using qualifiers that reference only one-side of join-loop inside of view definition.
The point of this scenario is criteria to distribute qualifiers of scanning-plan distributed in distribute_qual_to_rels(). If and when a qualifiers that reference only one-side of join-loop, the optimizer may distribute this qualifier into inside of the join-loop, even if it goes over the boundary of a subquery expanded from a view for row-level security.
This behavior allows us to reference whole of one-side of join-loop using functions with side-effects.
The solution is quite simple; it prohibits to distribute qualifiers over the boundary of subquery, however, performance cost is unignorable, because it also disables to utilize obviously indexable qualifiers such as (id=123), so this patch requires users a hint whether a particular view is for row-level security, or not.
This patch newly adds "CREATE SECURITY VIEW" statement that marks a flag to show this view was defined for row-level security purpose. This flag shall be stored as reloptions.
If this flag was set, the optimizer does not distribute qualifiers over the boundary of subqueries expanded from security views, except for obviously safe qualifiers.
(Right now, we consider built-in indexable operators are safe, but it might be arguable.)
It fixes up the scenario [2]unexpected push-down of functions with side-effect into join-loop in the bellow descriprions.
--------
The background of the leaky-view problem is well summarized at:
http://wiki.postgresql.org/wiki/RLS
We had discussed several scenarios in v9.1 development cycle, and the last developer meeting. We almost concluded the following criteria to characterize whether a leak-view scenario is problematic to be fixed, or not.
* If unprived user can directly reference contents of invisible tuples, it is a problem to be fixed.
* As long as contents of invisible tuples are consumed by internal stuff (eg, index-access method), it is not a problem to be fixed.
Thus, the scenario [1]unexpected reorder of functions with tiny-cost and side-effects and [2]unexpected push-down of functions with side-effect into join-loop are problematic to be fixed, but [3]estimation of hidden value using iteration of PK/FK proves and [4]estimation of hidden value using statistics are not. So, I'll try to fix up these two scenario with the patch part-1 amd part-2.
[1]: unexpected reorder of functions with tiny-cost and side-effects
Qualifiers of WHERE or JOIN ... IN clause shall be sorted by estimated cost, not depth of nest level. Thus, this logic can make order reversal when user-given qualifier has smaller cost than qualifiers to perform as security policy inside of view.
In the result, these qualifiers can reference both of visible and invisible tuples prior to the filtering by row-level security policy of the view. Thus, this behavior can be used to leak contents of invisible tuples.
[2]: unexpected push-down of functions with side-effect into join-loop
If arguments of qualifier being appended on outside of join-loop references only one-side of the join-loop, it is a good strategy to distribute this qualifier into inside of the join-loop to minimize number of tuples to be joined, from the viewpoint of performance.
However, it also makes order reversal when the join-loop is a part of view definition that should perform row-level security policy. Then, these exogenetic qualifiers may be executed prior to the filtering by row-level security policy of the view. Thus, this behavior can be used to leak contents of invisible tuple.
[3]: estimation of hidden value using iteration of PK/FK proves
Due to the nature of PK/FK constraints, we can infer existence of key values being stored within invisible tuple, even if we never allows users to reference contents of invisible tuples.
We commonly call this type of information leaks "covert-channel", and it is basically impossible to prevent according to the previous security research, however, its risk is also relatively small because of slow bandwidth to leak.
We already made consensus this scenario is not a problem to be fixed.
[4]: estimation of hidden value using statistics
One example was selectivity-estimator function; that may reference statistical information delivered from the tables have invisible tuples for optimization. Here are two points to be considered. The one is purely internal stuff may be able to reference invisible tuples, however, it is not a problem as long as it does not leak them into end-users; such as index access methods. The second is statistical or other form of date delivered from invisible tuples. We can set up a table that contains data delivered from invisible tuples using row-level triggers, however, it is quite a matter of database administration. Unless owner of tables set up such a leakable configuration, other users cannot reference them.
Thanks,
--
NEC Europe Ltd, SAP Global Competence Center
KaiGai Kohei <kohei.kaigai@emea.nec.com>
Attachments:
pgsql-fix-leaky-view-part-2.patchapplication/octet-stream; name=pgsql-fix-leaky-view-part-2.patchDownload
doc/src/sgml/ref/create_view.sgml | 22 ++++++-
doc/src/sgml/rules.sgml | 61 ++++++++++++++++
src/backend/access/common/reloptions.c | 15 ++++-
src/backend/commands/tablecmds.c | 3 +-
src/backend/commands/view.c | 34 +++++++--
src/backend/nodes/copyfuncs.c | 2 +
src/backend/nodes/equalfuncs.c | 2 +
src/backend/nodes/outfuncs.c | 2 +
src/backend/nodes/readfuncs.c | 2 +
src/backend/optimizer/plan/initsplan.c | 96 +++++++++++++++++++------
src/backend/optimizer/prep/prepjointree.c | 6 ++
src/backend/optimizer/util/clauses.c | 113 +++++++++++++++++++++++++++++
src/backend/parser/gram.y | 26 +++++---
src/backend/rewrite/rewriteHandler.c | 1 +
src/backend/utils/cache/lsyscache.c | 19 +++++
src/backend/utils/cache/relcache.c | 1 +
src/include/access/reloptions.h | 3 +-
src/include/nodes/parsenodes.h | 3 +
src/include/nodes/primnodes.h | 1 +
src/include/optimizer/clauses.h | 1 +
src/include/utils/lsyscache.h | 1 +
src/include/utils/rel.h | 11 +++-
src/test/regress/expected/create_view.out | 58 +++++++++++++++-
src/test/regress/sql/create_view.sql | 21 ++++++
24 files changed, 462 insertions(+), 42 deletions(-)
diff --git a/doc/src/sgml/ref/create_view.sgml b/doc/src/sgml/ref/create_view.sgml
index 417f8c3..a76e646 100644
--- a/doc/src/sgml/ref/create_view.sgml
+++ b/doc/src/sgml/ref/create_view.sgml
@@ -21,7 +21,7 @@ PostgreSQL documentation
<refsynopsisdiv>
<synopsis>
-CREATE [ OR REPLACE ] [ TEMP | TEMPORARY ] VIEW <replaceable class="PARAMETER">name</replaceable> [ ( <replaceable class="PARAMETER">column_name</replaceable> [, ...] ) ]
+CREATE [ OR REPLACE ] [ TEMP | TEMPORARY ] [SECURITY] VIEW <replaceable class="PARAMETER">name</replaceable> [ ( <replaceable class="PARAMETER">column_name</replaceable> [, ...] ) ]
AS <replaceable class="PARAMETER">query</replaceable>
</synopsis>
</refsynopsisdiv>
@@ -80,6 +80,26 @@ CREATE [ OR REPLACE ] [ TEMP | TEMPORARY ] VIEW <replaceable class="PARAMETER">n
</varlistentry>
<varlistentry>
+ <term><literal>SECURITY</literal></term>
+ <listitem>
+ <para>
+ If specified, a part of query optimization shall be restricted
+ to prevent unexpected information leaks.
+ We recommend to apply this option when the view is defined for
+ row-level security purpose, in spite of performance trade-off.
+ </para>
+ <para>
+ It is a commonly-used technique that using views to filter out
+ tuple to be invisible to particular users, however, please note
+ that here is a known-problem that allows malicious users to
+ reference invisible tuples using a function with side-effect
+ because of interaction with query optimization.
+ See <xref linkend="rules-privileges"> for more detailed scenario.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
<term><replaceable class="parameter">name</replaceable></term>
<listitem>
<para>
diff --git a/doc/src/sgml/rules.sgml b/doc/src/sgml/rules.sgml
index 1b06519..9905044 100644
--- a/doc/src/sgml/rules.sgml
+++ b/doc/src/sgml/rules.sgml
@@ -1856,6 +1856,67 @@ SELECT * FROM phone_number WHERE tricky(person, phone);
</para>
<para>
+ In addition, you might be able to leak contents of invisible tuples
+ using the following scenario:
+<programlisting>
+CREATE VIEW your_credit AS
+ SELECT a.rolname, c.number, c.expire
+ FROM pg_authid a JOIN credit_cards c ON a.oid = c.id
+ WHERE a.rolname = getpgusername();
+</programlisting>
+ This view also might seem secure, since any <command>SELECT</command>
+ from <literal>your_credit</literal> shall be rewritten into a
+ <command>SELECT</command> from the join of <literal>pg_authid</>
+ and <literal>credit_cards</> with a qualifier that filters out
+ any entries except for your credit card number.
+
+ But if a user appends his or her own functions that references
+ only columns come from a particular side of join loop, the optimizer
+ shall relocate this qualifier into the most deep level, independent
+ from cost estimation of the function.
+<programlisting>
+postgres=> SELECT * FROM your_credit WHERE tricky(number, expire);
+NOTICE: 1111-2222-3333-4444 => Jan-01
+NOTICE: 5555-6666-7777-8888 => Feb-02
+NOTICE: 1234-5678-9012-3456 => Mar-03
+ rolname | number | expire
+---------+---------------------+--------
+ alice | 5555-6666-7777-8888 | Feb-02
+(1 row)
+</programlisting>
+ The reason is obvious from the result of <command>EXPLAIN</command>.
+<programlisting>
+postgres=> EXPLAIN SELECT * FROM your_credit WHERE tricky(number, expire);
+ QUERY PLAN
+------------------------------------------------------------------------
+ Hash Join (cost=1.03..20.38 rows=1 width=128)
+ Hash Cond: (c.id = a.oid)
+ -> Seq Scan on credit_cards c (cost=0.00..18.30 rows=277 width=68)
+ Filter: tricky(number, expire)
+ -> Hash (cost=1.01..1.01 rows=1 width=68)
+ -> Seq Scan on pg_authid a (cost=0.00..1.01 rows=1 width=68)
+ Filter: (rolname = getpgusername())
+(7 rows)
+</programlisting>
+ The supplied <function>tricky</function> only references
+ <literal>number</literal> and <literal>expire</literal> columns,
+ however, the qualifier to filter invisible tuples performs on
+ the scan of <literal>pg_authid</literal>.
+ Then, since the optimizer tries to minimize the number of tuples
+ being joined, the supplied qualifer got attached on the scan of
+ <literal>credit_cards</literal>.
+ In the result, it allows <function>tricky</function> to reference
+ contents of the <literal>credit_cards</literal> table.
+</para>
+<para>
+ The <command>CREATE SECURITY VIEW</command> enables to prevent
+ unexpected push-down of qualifiers supplied from outside of the
+ view, however, of course, it is not a fully optimized query plan
+ from the perspective of performance.
+ So, pay attention to the trade-off between security and performance.
+</para>
+
+<para>
Similar considerations apply to update rules. In the examples of
the previous section, the owner of the tables in the example
database could grant the privileges <literal>SELECT</>,
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 4657425..9c3669b 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -66,6 +66,14 @@ static relopt_bool boolRelOpts[] =
},
true
},
+ {
+ {
+ "security_view",
+ "Restrict maximum optimization to prevent data leaks",
+ RELOPT_KIND_VIEW
+ },
+ false
+ },
/* list terminator */
{{NULL}}
};
@@ -776,6 +784,7 @@ extractRelOptions(HeapTuple tuple, TupleDesc tupdesc, Oid amoptions)
{
case RELKIND_RELATION:
case RELKIND_TOASTVALUE:
+ case RELKIND_VIEW:
case RELKIND_UNCATALOGED:
options = heap_reloptions(classForm->relkind, datum, false);
break;
@@ -1134,7 +1143,9 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
{"autovacuum_vacuum_scale_factor", RELOPT_TYPE_REAL,
offsetof(StdRdOptions, autovacuum) +offsetof(AutoVacOpts, vacuum_scale_factor)},
{"autovacuum_analyze_scale_factor", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) +offsetof(AutoVacOpts, analyze_scale_factor)}
+ offsetof(StdRdOptions, autovacuum) +offsetof(AutoVacOpts, analyze_scale_factor)},
+ {"security_view", RELOPT_TYPE_BOOL,
+ offsetof(StdRdOptions, security_view)},
};
options = parseRelOptions(reloptions, validate, kind, &numoptions);
@@ -1176,6 +1187,8 @@ heap_reloptions(char relkind, Datum reloptions, bool validate)
return (bytea *) rdopts;
case RELKIND_RELATION:
return default_reloptions(reloptions, validate, RELOPT_KIND_HEAP);
+ case RELKIND_VIEW:
+ return default_reloptions(reloptions, validate, RELOPT_KIND_VIEW);
default:
/* other relkinds are not supported */
return NULL;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index bdbcdff..4649b59 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -2911,7 +2911,7 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
break;
case AT_SetRelOptions: /* SET (...) */
case AT_ResetRelOptions: /* RESET (...) */
- ATSimplePermissions(rel, ATT_TABLE | ATT_INDEX);
+ ATSimplePermissions(rel, ATT_TABLE | ATT_INDEX | ATT_VIEW);
/* This command never recurses */
/* No command-specific prep needed */
pass = AT_PASS_MISC;
@@ -7686,6 +7686,7 @@ ATExecSetRelOptions(Relation rel, List *defList, bool isReset, LOCKMODE lockmode
{
case RELKIND_RELATION:
case RELKIND_TOASTVALUE:
+ case RELKIND_VIEW:
(void) heap_reloptions(rel->rd_rel->relkind, newOptions, true);
break;
case RELKIND_INDEX:
diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c
index be681e3..e1449d1 100644
--- a/src/backend/commands/view.c
+++ b/src/backend/commands/view.c
@@ -97,7 +97,8 @@ isViewOnTempTable_walker(Node *node, void *context)
*---------------------------------------------------------------------
*/
static Oid
-DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace)
+DefineVirtualRelation(const RangeVar *relation, List *tlist,
+ bool replace, bool security)
{
Oid viewOid,
namespaceId;
@@ -167,6 +168,8 @@ DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace)
{
Relation rel;
TupleDesc descriptor;
+ List *atcmds = NIL;
+ AlterTableCmd *atcmd;
/*
* Yes. Get exclusive lock on the existing view ...
@@ -205,20 +208,32 @@ DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace)
checkViewTupleDesc(descriptor, rel->rd_att);
/*
+ * If "SECURITY" flag is not compatible to the view being replaced,
+ * we need to modify "security_view" reloption here.
+ */
+ if (RelationIsSecurityView(rel) != security)
+ {
+ DefElem *defel = makeDefElem("security_view", NULL);
+
+ atcmd = makeNode(AlterTableCmd);
+ atcmd->subtype = security ? AT_SetRelOptions : AT_ResetRelOptions;
+ atcmd->def = list_make1((Node *) defel);
+
+ atcmds = lappend(atcmds, atcmd);
+ }
+
+ /*
* If new attributes have been added, we must add pg_attribute entries
* for them. It is convenient (although overkill) to use the ALTER
* TABLE ADD COLUMN infrastructure for this.
*/
if (list_length(attrList) > rel->rd_att->natts)
{
- List *atcmds = NIL;
ListCell *c;
int skip = rel->rd_att->natts;
foreach(c, attrList)
{
- AlterTableCmd *atcmd;
-
if (skip > 0)
{
skip--;
@@ -229,8 +244,9 @@ DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace)
atcmd->def = (Node *) lfirst(c);
atcmds = lappend(atcmds, atcmd);
}
- AlterTableInternal(viewOid, atcmds, true);
}
+ if (atcmds != NIL)
+ AlterTableInternal(viewOid, atcmds, true);
/*
* Seems okay, so return the OID of the pre-existing view.
@@ -256,6 +272,12 @@ DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace)
createStmt->tablespacename = NULL;
createStmt->if_not_exists = false;
+ if (security)
+ {
+ DefElem *defel = makeDefElem("security_view", NULL);
+ createStmt->options = lappend(createStmt->options, defel);
+ }
+
/*
* finally create the relation (this will error out if there's an
* existing view, so we don't need more code to complain if "replace"
@@ -510,7 +532,7 @@ DefineView(ViewStmt *stmt, const char *queryString)
* aborted.
*/
viewOid = DefineVirtualRelation(view, viewParse->targetList,
- stmt->replace);
+ stmt->replace, stmt->security);
/*
* The relation we have just created is not visible to any other commands
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index c9133dd..ce762cc 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -1789,6 +1789,7 @@ _copyFromExpr(FromExpr *from)
COPY_NODE_FIELD(fromlist);
COPY_NODE_FIELD(quals);
+ COPY_SCALAR_FIELD(security_view);
return newnode;
}
@@ -1944,6 +1945,7 @@ _copyRangeTblEntry(RangeTblEntry *from)
COPY_SCALAR_FIELD(relid);
COPY_SCALAR_FIELD(relkind);
COPY_NODE_FIELD(subquery);
+ COPY_SCALAR_FIELD(security_view);
COPY_SCALAR_FIELD(jointype);
COPY_NODE_FIELD(joinaliasvars);
COPY_NODE_FIELD(funcexpr);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 3a0267c..89cf310 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -772,6 +772,7 @@ _equalFromExpr(FromExpr *a, FromExpr *b)
{
COMPARE_NODE_FIELD(fromlist);
COMPARE_NODE_FIELD(quals);
+ COMPARE_SCALAR_FIELD(security_view);
return true;
}
@@ -2303,6 +2304,7 @@ _equalRangeTblEntry(RangeTblEntry *a, RangeTblEntry *b)
COMPARE_SCALAR_FIELD(relid);
COMPARE_SCALAR_FIELD(relkind);
COMPARE_NODE_FIELD(subquery);
+ COMPARE_SCALAR_FIELD(security_view);
COMPARE_SCALAR_FIELD(jointype);
COMPARE_NODE_FIELD(joinaliasvars);
COMPARE_NODE_FIELD(funcexpr);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 681f5f8..3ad7067 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -1447,6 +1447,7 @@ _outFromExpr(StringInfo str, FromExpr *node)
WRITE_NODE_FIELD(fromlist);
WRITE_NODE_FIELD(quals);
+ WRITE_BOOL_FIELD(security_view);
}
/*****************************************************************************
@@ -2311,6 +2312,7 @@ _outRangeTblEntry(StringInfo str, RangeTblEntry *node)
break;
case RTE_SUBQUERY:
WRITE_NODE_FIELD(subquery);
+ WRITE_BOOL_FIELD(security_view);
break;
case RTE_JOIN:
WRITE_ENUM_FIELD(jointype, JoinType);
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 2288514..a63236d 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -1160,6 +1160,7 @@ _readFromExpr(void)
READ_NODE_FIELD(fromlist);
READ_NODE_FIELD(quals);
+ READ_BOOL_FIELD(security_view);
READ_DONE();
}
@@ -1190,6 +1191,7 @@ _readRangeTblEntry(void)
break;
case RTE_SUBQUERY:
READ_NODE_FIELD(subquery);
+ READ_BOOL_FIELD(security_view);
break;
case RTE_JOIN:
READ_ENUM_FIELD(jointype, JoinType);
diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c
index 333ede2..628a84e 100644
--- a/src/backend/optimizer/plan/initsplan.c
+++ b/src/backend/optimizer/plan/initsplan.c
@@ -41,7 +41,8 @@ int join_collapse_limit;
static List *deconstruct_recurse(PlannerInfo *root, Node *jtnode,
bool below_outer_join,
- Relids *qualscope, Relids *inner_join_rels);
+ Relids *qualscope, Relids *inner_join_rels,
+ bool below_sec_barriers, Relids *sec_barriers);
static SpecialJoinInfo *make_outerjoininfo(PlannerInfo *root,
Relids left_rels, Relids right_rels,
Relids inner_join_rels,
@@ -52,7 +53,8 @@ static void distribute_qual_to_rels(PlannerInfo *root, Node *clause,
JoinType jointype,
Relids qualscope,
Relids ojscope,
- Relids outerjoin_nonnullable);
+ Relids outerjoin_nonnullable,
+ Relids sec_barriers);
static bool check_outerjoin_delay(PlannerInfo *root, Relids *relids_p,
Relids *nullable_relids_p, bool is_pushed_down);
static bool check_redundant_nullability_qual(PlannerInfo *root, Node *clause);
@@ -240,13 +242,15 @@ deconstruct_jointree(PlannerInfo *root)
{
Relids qualscope;
Relids inner_join_rels;
+ Relids sec_barriers;
+ FromExpr *f = (FromExpr *)root->parse->jointree;
/* Start recursion at top of jointree */
- Assert(root->parse->jointree != NULL &&
- IsA(root->parse->jointree, FromExpr));
+ Assert(root->parse->jointree != NULL && IsA(f, FromExpr));
- return deconstruct_recurse(root, (Node *) root->parse->jointree, false,
- &qualscope, &inner_join_rels);
+ return deconstruct_recurse(root, (Node *) f, false,
+ &qualscope, &inner_join_rels,
+ f->security_view, &sec_barriers);
}
/*
@@ -270,7 +274,8 @@ deconstruct_jointree(PlannerInfo *root)
*/
static List *
deconstruct_recurse(PlannerInfo *root, Node *jtnode, bool below_outer_join,
- Relids *qualscope, Relids *inner_join_rels)
+ Relids *qualscope, Relids *inner_join_rels,
+ bool below_sec_barriers, Relids *sec_barriers)
{
List *joinlist;
@@ -289,6 +294,9 @@ deconstruct_recurse(PlannerInfo *root, Node *jtnode, bool below_outer_join,
/* A single baserel does not create an inner join */
*inner_join_rels = NULL;
joinlist = list_make1(jtnode);
+ /* Is it in security barrier? */
+ *sec_barriers = (below_sec_barriers ?
+ bms_make_singleton(varno) : NULL);
}
else if (IsA(jtnode, FromExpr))
{
@@ -304,6 +312,7 @@ deconstruct_recurse(PlannerInfo *root, Node *jtnode, bool below_outer_join,
*/
*qualscope = NULL;
*inner_join_rels = NULL;
+ *sec_barriers = NULL;
joinlist = NIL;
remaining = list_length(f->fromlist);
foreach(l, f->fromlist)
@@ -311,12 +320,17 @@ deconstruct_recurse(PlannerInfo *root, Node *jtnode, bool below_outer_join,
Relids sub_qualscope;
List *sub_joinlist;
int sub_members;
+ Relids sub_barriers;
sub_joinlist = deconstruct_recurse(root, lfirst(l),
below_outer_join,
&sub_qualscope,
- inner_join_rels);
+ inner_join_rels,
+ below_sec_barriers ?
+ true : f->security_view,
+ &sub_barriers);
*qualscope = bms_add_members(*qualscope, sub_qualscope);
+ *sec_barriers = bms_add_members(*sec_barriers, sub_barriers);
sub_members = list_length(sub_joinlist);
remaining--;
if (sub_members <= 1 ||
@@ -345,7 +359,7 @@ deconstruct_recurse(PlannerInfo *root, Node *jtnode, bool below_outer_join,
distribute_qual_to_rels(root, qual,
false, below_outer_join, JOIN_INNER,
- *qualscope, NULL, NULL);
+ *qualscope, NULL, NULL, *sec_barriers);
}
}
else if (IsA(jtnode, JoinExpr))
@@ -355,6 +369,8 @@ deconstruct_recurse(PlannerInfo *root, Node *jtnode, bool below_outer_join,
rightids,
left_inners,
right_inners,
+ left_barriers,
+ right_barriers,
nonnullable_rels,
ojscope;
List *leftjoinlist,
@@ -379,12 +395,17 @@ deconstruct_recurse(PlannerInfo *root, Node *jtnode, bool below_outer_join,
case JOIN_INNER:
leftjoinlist = deconstruct_recurse(root, j->larg,
below_outer_join,
- &leftids, &left_inners);
+ &leftids, &left_inners,
+ below_sec_barriers,
+ &left_barriers);
rightjoinlist = deconstruct_recurse(root, j->rarg,
below_outer_join,
- &rightids, &right_inners);
+ &rightids, &right_inners,
+ below_sec_barriers,
+ &right_barriers);
*qualscope = bms_union(leftids, rightids);
*inner_join_rels = *qualscope;
+ *sec_barriers = bms_union(left_barriers, right_barriers);
/* Inner join adds no restrictions for quals */
nonnullable_rels = NULL;
break;
@@ -392,35 +413,50 @@ deconstruct_recurse(PlannerInfo *root, Node *jtnode, bool below_outer_join,
case JOIN_ANTI:
leftjoinlist = deconstruct_recurse(root, j->larg,
below_outer_join,
- &leftids, &left_inners);
+ &leftids, &left_inners,
+ below_sec_barriers,
+ &left_barriers);
rightjoinlist = deconstruct_recurse(root, j->rarg,
true,
- &rightids, &right_inners);
+ &rightids, &right_inners,
+ below_sec_barriers,
+ &right_barriers);
*qualscope = bms_union(leftids, rightids);
*inner_join_rels = bms_union(left_inners, right_inners);
+ *sec_barriers = bms_union(left_barriers, right_barriers);
nonnullable_rels = leftids;
break;
case JOIN_SEMI:
leftjoinlist = deconstruct_recurse(root, j->larg,
below_outer_join,
- &leftids, &left_inners);
+ &leftids, &left_inners,
+ below_sec_barriers,
+ &left_barriers);
rightjoinlist = deconstruct_recurse(root, j->rarg,
below_outer_join,
- &rightids, &right_inners);
+ &rightids, &right_inners,
+ below_sec_barriers,
+ &right_barriers);
*qualscope = bms_union(leftids, rightids);
*inner_join_rels = bms_union(left_inners, right_inners);
+ *sec_barriers = bms_union(left_barriers, right_barriers);
/* Semi join adds no restrictions for quals */
nonnullable_rels = NULL;
break;
case JOIN_FULL:
leftjoinlist = deconstruct_recurse(root, j->larg,
true,
- &leftids, &left_inners);
+ &leftids, &left_inners,
+ below_sec_barriers,
+ &left_barriers);
rightjoinlist = deconstruct_recurse(root, j->rarg,
true,
- &rightids, &right_inners);
+ &rightids, &right_inners,
+ below_sec_barriers,
+ &right_barriers);
*qualscope = bms_union(leftids, rightids);
*inner_join_rels = bms_union(left_inners, right_inners);
+ *sec_barriers = bms_union(left_barriers, right_barriers);
/* each side is both outer and inner */
nonnullable_rels = *qualscope;
break;
@@ -469,7 +505,8 @@ deconstruct_recurse(PlannerInfo *root, Node *jtnode, bool below_outer_join,
distribute_qual_to_rels(root, qual,
false, below_outer_join, j->jointype,
*qualscope,
- ojscope, nonnullable_rels);
+ ojscope, nonnullable_rels,
+ *sec_barriers);
}
/* Now we can add the SpecialJoinInfo to join_info_list */
@@ -793,7 +830,8 @@ distribute_qual_to_rels(PlannerInfo *root, Node *clause,
JoinType jointype,
Relids qualscope,
Relids ojscope,
- Relids outerjoin_nonnullable)
+ Relids outerjoin_nonnullable,
+ Relids sec_barriers)
{
Relids relids;
bool is_pushed_down;
@@ -801,6 +839,7 @@ distribute_qual_to_rels(PlannerInfo *root, Node *clause,
bool pseudoconstant = false;
bool maybe_equivalence;
bool maybe_outer_join;
+ bool maybe_leakable_clause = false;
Relids nullable_relids;
RestrictInfo *restrictinfo;
@@ -873,6 +912,21 @@ distribute_qual_to_rels(PlannerInfo *root, Node *clause,
}
}
+ /*
+ * If and when the supplied clause contains a leakable functions,
+ * it might be used to bypass row-level security using views.
+ * In this case, we should not push down the clause to prevent
+ * the leakable clause being evaluated prior to row-level policy
+ * functions.
+ */
+ if (!bms_is_empty(sec_barriers) &&
+ contain_leakable_functions(clause) &&
+ bms_overlap(relids, sec_barriers))
+ {
+ maybe_leakable_clause = true;
+ relids = bms_add_members(relids, sec_barriers);
+ }
+
/*----------
* Check to see if clause application must be delayed by outer-join
* considerations.
@@ -1075,7 +1129,7 @@ distribute_qual_to_rels(PlannerInfo *root, Node *clause,
* process_equivalence is successful, it will take care of that;
* otherwise, we have to call initialize_mergeclause_eclasses to do it.
*/
- if (restrictinfo->mergeopfamilies)
+ if (!maybe_leakable_clause && restrictinfo->mergeopfamilies)
{
if (maybe_equivalence)
{
@@ -1417,7 +1471,7 @@ process_implied_equality(PlannerInfo *root,
*/
distribute_qual_to_rels(root, (Node *) clause,
true, below_outer_join, JOIN_INNER,
- qualscope, NULL, NULL);
+ qualscope, NULL, NULL, NULL);
}
/*
diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
index 5d16329..bc23936 100644
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -699,6 +699,12 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte,
pull_up_subqueries(subroot, (Node *) subquery->jointree, NULL, NULL);
/*
+ * If the sub-query is originated from security-view, we mark it here
+ * to prevent too-much optimization in later phase.
+ */
+ ((FromExpr *) subquery->jointree)->security_view = rte->security_view;
+
+ /*
* Now we must recheck whether the subquery is still simple enough to pull
* up. If not, abandon processing it.
*
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index 8b0d862..3e0b2e8 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -93,6 +93,7 @@ static bool contain_subplans_walker(Node *node, void *context);
static bool contain_mutable_functions_walker(Node *node, void *context);
static bool contain_volatile_functions_walker(Node *node, void *context);
static bool contain_nonstrict_functions_walker(Node *node, void *context);
+static bool contain_leakable_functions_walker(Node *node, void *context);
static Relids find_nonnullable_rels_walker(Node *node, bool top_level);
static List *find_nonnullable_vars_walker(Node *node, bool top_level);
static bool is_strict_saop(ScalarArrayOpExpr *expr, bool falseOK);
@@ -1164,6 +1165,118 @@ contain_nonstrict_functions_walker(Node *node, void *context)
context);
}
+/*****************************************************************************
+ * Check clauses for leakable functions
+ *****************************************************************************/
+
+/*
+ * contain_leakable_functions
+ * Recursively search for leakable functions within a clause.
+ *
+ * Returns true if any function call with side-effect is found.
+ * ie, some type-input/output handler will raise an error when given
+ * argument does not have a valid format.
+ *
+ * When people uses views for row-level security purpose, given qualifiers
+ * come from outside of the view should not be pushed down into the views,
+ * if they have side-effect, because contents of tuples to be filtered out
+ * may be leaked via side-effectable functions within the qualifiers.
+ *
+ * The idea here is that the planner restrain a part of optimization when
+ * the qualifiers contains leakable functions.
+ * This routine checks whether the given clause contains leakable functions,
+ * or not. If we return false, then the clause is clean.
+ */
+bool
+contain_leakable_functions(Node *clause)
+{
+ return contain_leakable_functions_walker(clause, NULL);
+}
+
+static bool
+contain_leakable_functions_walker(Node *node, void *context)
+{
+ if (node == NULL)
+ return false;
+
+ if (IsA(node, FuncExpr))
+ {
+ /*
+ * currently, we have no way to distinguish a safe function and
+ * a leakable one, so all the function call shall be considered
+ * as leakable one.
+ */
+ return true;
+ }
+ else if (IsA(node, OpExpr))
+ {
+ OpExpr *expr = (OpExpr *) node;
+
+ /*
+ * we assume built-in functions to implement operators are not
+ * leakable, so don't need to prevent optimization.
+ */
+ set_opfuncid(expr);
+ if (get_func_lang(expr->opfuncid) != INTERNALlanguageId)
+ return true;
+ /* else fall through to check args */
+ }
+ else if (IsA(node, DistinctExpr))
+ {
+ DistinctExpr *expr = (DistinctExpr *) node;
+
+ set_opfuncid((OpExpr *) expr);
+ if (get_func_lang(expr->opfuncid) != INTERNALlanguageId)
+ return true;
+ /* else fall through to check args */
+ }
+ else if (IsA(node, ScalarArrayOpExpr))
+ {
+ ScalarArrayOpExpr *expr = (ScalarArrayOpExpr *) node;
+
+ set_sa_opfuncid(expr);
+ if (get_func_lang(expr->opfuncid) != INTERNALlanguageId)
+ return true;
+ /* else fall through to check args */
+ }
+ else if (IsA(node, CoerceViaIO) ||
+ IsA(node, ArrayCoerceExpr))
+ {
+ /*
+ * we assume type-in/out handlers are leakable, even if built-in
+ * functions.
+ * ie, int4in() raises an error message with given argument,
+ * if it does not have valid format for numeric value.
+ */
+ return true;
+ }
+ else if (IsA(node, NullIfExpr))
+ {
+ NullIfExpr *expr = (NullIfExpr *) node;
+
+ set_opfuncid((OpExpr *) expr); /* rely on struct equivalence */
+ if (get_func_lang(expr->opfuncid) != INTERNALlanguageId)
+ return true;
+ /* else fall through to check args */
+ }
+ else if (IsA(node, RowCompareExpr))
+ {
+ /* RowCompare probably can't have volatile ops, but check anyway */
+ RowCompareExpr *rcexpr = (RowCompareExpr *) node;
+ ListCell *opid;
+
+ foreach(opid, rcexpr->opnos)
+ {
+ Oid funcId = get_opcode(lfirst_oid(opid));
+
+ if (get_func_lang(funcId) != INTERNALlanguageId)
+ return true;
+ }
+ /* else fall through to check args */
+ }
+ return expression_tree_walker(node, contain_leakable_functions_walker,
+ context);
+}
/*
* find_nonnullable_rels
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 1d39674..ebe5dff 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -325,6 +325,7 @@ static void SplitColQualList(List *qualList,
%type <boolean> opt_trusted opt_restart_seqs
%type <ival> OptTemp
+%type <boolean> OptSecurity
%type <oncommit> OnCommitOption
%type <node> for_locking_item
@@ -7264,35 +7265,42 @@ transaction_mode_list_or_empty:
/*****************************************************************************
*
* QUERY:
- * CREATE [ OR REPLACE ] [ TEMP ] VIEW <viewname> '('target-list ')'
+ * CREATE [ OR REPLACE ] [ TEMP ] [ SECURITY ]
+ * VIEW <viewname> '('target-list ')'
* AS <query> [ WITH [ CASCADED | LOCAL ] CHECK OPTION ]
*
*****************************************************************************/
-ViewStmt: CREATE OptTemp VIEW qualified_name opt_column_list
+ViewStmt: CREATE OptTemp OptSecurity VIEW qualified_name opt_column_list
AS SelectStmt opt_check_option
{
ViewStmt *n = makeNode(ViewStmt);
- n->view = $4;
+ n->view = $5;
n->view->relpersistence = $2;
- n->aliases = $5;
- n->query = $7;
+ n->aliases = $6;
+ n->query = $8;
n->replace = false;
+ n->security = $3;
$$ = (Node *) n;
}
- | CREATE OR REPLACE OptTemp VIEW qualified_name opt_column_list
+ | CREATE OR REPLACE OptTemp OptSecurity VIEW qualified_name opt_column_list
AS SelectStmt opt_check_option
{
ViewStmt *n = makeNode(ViewStmt);
- n->view = $6;
+ n->view = $7;
n->view->relpersistence = $4;
- n->aliases = $7;
- n->query = $9;
+ n->aliases = $8;
+ n->query = $10;
n->replace = true;
+ n->security = $5;
$$ = (Node *) n;
}
;
+OptSecurity: SECURITY { $$ = TRUE; }
+ | /* EMPTY */ { $$ = FALSE; }
+ ;
+
opt_check_option:
WITH CHECK OPTION
{
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index bfc8fd7..7d25e39 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -1344,6 +1344,7 @@ ApplyRetrieveRule(Query *parsetree,
rte->rtekind = RTE_SUBQUERY;
rte->relid = InvalidOid;
rte->subquery = rule_action;
+ rte->security_view = RelationIsSecurityView(relation);
rte->inh = false; /* must not be set for a subquery */
/*
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index d3b2a5a..a7e4d3e 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -1390,6 +1390,25 @@ get_func_namespace(Oid funcid)
}
/*
+ * get_func_lang
+ * Given procedure id, return the function's language
+ */
+Oid
+get_func_lang(Oid funcid)
+{
+ HeapTuple tp;
+ Oid result;
+
+ tp = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcid));
+ if (!HeapTupleIsValid(tp))
+ elog(ERROR, "cache lookup failed for function %u", funcid);
+
+ result = ((Form_pg_proc) GETSTRUCT(tp))->prolang;
+ ReleaseSysCache(tp);
+ return result;
+}
+
+/*
* get_func_rettype
* Given procedure id, return the function's result type.
*/
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index d7e94ff..ab8146a 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -377,6 +377,7 @@ RelationParseRelOptions(Relation relation, HeapTuple tuple)
case RELKIND_RELATION:
case RELKIND_TOASTVALUE:
case RELKIND_INDEX:
+ case RELKIND_VIEW:
break;
default:
return;
diff --git a/src/include/access/reloptions.h b/src/include/access/reloptions.h
index c7709cc..586236e 100644
--- a/src/include/access/reloptions.h
+++ b/src/include/access/reloptions.h
@@ -42,8 +42,9 @@ typedef enum relopt_kind
RELOPT_KIND_GIST = (1 << 5),
RELOPT_KIND_ATTRIBUTE = (1 << 6),
RELOPT_KIND_TABLESPACE = (1 << 7),
+ RELOPT_KIND_VIEW = (1 << 8),
/* if you add a new kind, make sure you update "last_default" too */
- RELOPT_KIND_LAST_DEFAULT = RELOPT_KIND_TABLESPACE,
+ RELOPT_KIND_LAST_DEFAULT = RELOPT_KIND_VIEW,
/* some compilers treat enums as signed ints, so we can't use 1 << 31 */
RELOPT_KIND_MAX = (1 << 30)
} relopt_kind;
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index ee1881b..e274984 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -706,6 +706,8 @@ typedef struct RangeTblEntry
*/
Query *subquery; /* the sub-query */
+ bool security_view; /* Is the sub-query come from security view? */
+
/*
* Fields valid for a join RTE (else NULL/zero):
*
@@ -2336,6 +2338,7 @@ typedef struct ViewStmt
List *aliases; /* target column names */
Node *query; /* the SELECT query */
bool replace; /* replace an existing view? */
+ bool security; /* view is defined for row-level security? */
} ViewStmt;
/* ----------------------
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index f1e20ef..5e2a788 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -1259,6 +1259,7 @@ typedef struct FromExpr
NodeTag type;
List *fromlist; /* List of join subtrees */
Node *quals; /* qualifiers on join, if any */
+ bool security_view; /* It came from security views */
} FromExpr;
#endif /* PRIMNODES_H */
diff --git a/src/include/optimizer/clauses.h b/src/include/optimizer/clauses.h
index 4af772d..035bdf5 100644
--- a/src/include/optimizer/clauses.h
+++ b/src/include/optimizer/clauses.h
@@ -62,6 +62,7 @@ extern bool contain_subplans(Node *clause);
extern bool contain_mutable_functions(Node *clause);
extern bool contain_volatile_functions(Node *clause);
extern bool contain_nonstrict_functions(Node *clause);
+extern bool contain_leakable_functions(Node *clause);
extern Relids find_nonnullable_rels(Node *clause);
extern List *find_nonnullable_vars(Node *clause);
extern List *find_forced_null_vars(Node *clause);
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index 0a419dc..659cdc0 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -79,6 +79,7 @@ extern RegProcedure get_oprrest(Oid opno);
extern RegProcedure get_oprjoin(Oid opno);
extern char *get_func_name(Oid funcid);
extern Oid get_func_namespace(Oid funcid);
+extern Oid get_func_lang(Oid funcid);
extern Oid get_func_rettype(Oid funcid);
extern int get_func_nargs(Oid funcid);
extern Oid get_func_signature(Oid funcid, Oid **argtypes, int *nargs);
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index e2c2fa9..3f25949 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -221,7 +221,7 @@ typedef struct RelationData
/*
* StdRdOptions
- * Standard contents of rd_options for heaps and generic indexes.
+ * Standard contents of rd_options for heaps, views and generic indexes.
*
* RelationGetFillFactor() and RelationGetTargetPageFreeSpace() can only
* be applied to relations that use this format or a superset for
@@ -247,6 +247,7 @@ typedef struct StdRdOptions
int32 vl_len_; /* varlena header (do not touch directly!) */
int fillfactor; /* page fill factor in percent (0..100) */
AutoVacOpts autovacuum; /* autovacuum-related options */
+ bool security_view; /* restrict fully optimization, if true */
} StdRdOptions;
#define HEAP_MIN_FILLFACTOR 10
@@ -275,6 +276,14 @@ typedef struct StdRdOptions
(BLCKSZ * (100 - RelationGetFillFactor(relation, defaultff)) / 100)
/*
+ * RelationIsSecurityView
+ * Returns whether the relation is security view, or not
+ */
+#define RelationIsSecurityView(relation) \
+ ((relation)->rd_options ? \
+ ((StdRdOptions *) (relation)->rd_options)->security_view : false)
+
+/*
* RelationIsValid
* True iff relation descriptor is valid.
*/
diff --git a/src/test/regress/expected/create_view.out b/src/test/regress/expected/create_view.out
index f2c0685..ceaa701 100644
--- a/src/test/regress/expected/create_view.out
+++ b/src/test/regress/expected/create_view.out
@@ -239,6 +239,58 @@ And relnamespace IN (SELECT OID FROM pg_namespace WHERE nspname LIKE 'pg_temp%')
1
(1 row)
+--Should work correctly to leaky-view scenario
+CREATE TABLE lvtest1 (a int, b text);
+CREATE TABLE lvtest2 (x int, y text);
+CREATE FUNCTION f_leak(text) RETURNS bool LANGUAGE 'plpgsql'
+AS 'BEGIN raise notice ''leak => %'', $1; RETURN true; END';
+INSERT INTO lvtest1 VALUES (10, 'aaa'), (11, 'bbb'), (12, 'ccc'), (13, 'ddd');
+INSERT INTO lvtest2 VALUES (11, 'xxx'), (12, 'yyy'), (13, 'zzz'), (14, 'xyz');
+CREATE OR REPLACE VIEW leaky_view AS
+ SELECT * FROM lvtest1 JOIN lvtest2 ON a = x WHERE a % 2 = 0;
+SELECT * FROM leaky_view WHERE f_leak(y);
+NOTICE: leak => xxx
+NOTICE: leak => yyy
+NOTICE: leak => zzz
+NOTICE: leak => xyz
+ a | b | x | y
+----+-----+----+-----
+ 12 | ccc | 12 | yyy
+(1 row)
+
+EXPLAIN SELECT * FROM leaky_view WHERE f_leak(y);
+ QUERY PLAN
+---------------------------------------------------------------------
+ Hash Join (cost=28.52..359.98 rows=12 width=72)
+ Hash Cond: (lvtest2.x = lvtest1.a)
+ -> Seq Scan on lvtest2 (cost=0.00..329.80 rows=410 width=36)
+ Filter: f_leak(y)
+ -> Hash (cost=28.45..28.45 rows=6 width=36)
+ -> Seq Scan on lvtest1 (cost=0.00..28.45 rows=6 width=36)
+ Filter: ((a % 2) = 0)
+(7 rows)
+
+CREATE OR REPLACE SECURITY VIEW leaky_view AS
+ SELECT * FROM lvtest1 JOIN lvtest2 ON a = x WHERE a % 2 = 0;
+SELECT * FROM leaky_view WHERE f_leak(y);
+NOTICE: leak => yyy
+ a | b | x | y
+----+-----+----+-----
+ 12 | ccc | 12 | yyy
+(1 row)
+
+EXPLAIN SELECT * FROM leaky_view WHERE f_leak(y);
+ QUERY PLAN
+---------------------------------------------------------------------
+ Hash Join (cost=28.52..65.06 rows=12 width=72)
+ Hash Cond: (lvtest2.x = lvtest1.a)
+ Join Filter: f_leak(lvtest2.y)
+ -> Seq Scan on lvtest2 (cost=0.00..22.30 rows=1230 width=36)
+ -> Hash (cost=28.45..28.45 rows=6 width=36)
+ -> Seq Scan on lvtest1 (cost=0.00..28.45 rows=6 width=36)
+ Filter: ((a % 2) = 0)
+(7 rows)
+
DROP SCHEMA temp_view_test CASCADE;
NOTICE: drop cascades to 22 other objects
DETAIL: drop cascades to table temp_view_test.base_table
@@ -264,7 +316,7 @@ drop cascades to view temp_view_test.v8
drop cascades to sequence temp_view_test.seq1
drop cascades to view temp_view_test.v9
DROP SCHEMA testviewschm2 CASCADE;
-NOTICE: drop cascades to 16 other objects
+NOTICE: drop cascades to 20 other objects
DETAIL: drop cascades to table t1
drop cascades to view temporal1
drop cascades to view temporal2
@@ -281,4 +333,8 @@ drop cascades to table tbl3
drop cascades to table tbl4
drop cascades to view mytempview
drop cascades to view pubview
+drop cascades to table lvtest1
+drop cascades to table lvtest2
+drop cascades to function f_leak(text)
+drop cascades to view leaky_view
SET search_path to public;
diff --git a/src/test/regress/sql/create_view.sql b/src/test/regress/sql/create_view.sql
index 86cfc51..d9b7fa1 100644
--- a/src/test/regress/sql/create_view.sql
+++ b/src/test/regress/sql/create_view.sql
@@ -191,6 +191,27 @@ AND NOT EXISTS (SELECT g FROM tbl4 LEFT JOIN tmptbl ON tbl4.h = tmptbl.j);
SELECT count(*) FROM pg_class where relname LIKE 'mytempview'
And relnamespace IN (SELECT OID FROM pg_namespace WHERE nspname LIKE 'pg_temp%');
+--Should work correctly to leaky-view scenario
+CREATE TABLE lvtest1 (a int, b text);
+CREATE TABLE lvtest2 (x int, y text);
+CREATE FUNCTION f_leak(text) RETURNS bool LANGUAGE 'plpgsql'
+AS 'BEGIN raise notice ''leak => %'', $1; RETURN true; END';
+
+INSERT INTO lvtest1 VALUES (10, 'aaa'), (11, 'bbb'), (12, 'ccc'), (13, 'ddd');
+INSERT INTO lvtest2 VALUES (11, 'xxx'), (12, 'yyy'), (13, 'zzz'), (14, 'xyz');
+
+CREATE OR REPLACE VIEW leaky_view AS
+ SELECT * FROM lvtest1 JOIN lvtest2 ON a = x WHERE a % 2 = 0;
+
+SELECT * FROM leaky_view WHERE f_leak(y);
+EXPLAIN SELECT * FROM leaky_view WHERE f_leak(y);
+
+CREATE OR REPLACE SECURITY VIEW leaky_view AS
+ SELECT * FROM lvtest1 JOIN lvtest2 ON a = x WHERE a % 2 = 0;
+
+SELECT * FROM leaky_view WHERE f_leak(y);
+EXPLAIN SELECT * FROM leaky_view WHERE f_leak(y);
+
DROP SCHEMA temp_view_test CASCADE;
DROP SCHEMA testviewschm2 CASCADE;
I took a look at this patch. It's incredibly simple, which is great, and it
seems to achieve its goal.
Suppose your query references two views owned by different roles. The quals
of those views will have the same depth. Is there a way for information to
leak from one view owner to another due to that?
I like how you've assumed that the table owner trusts the operator class
functions of indexes on his table to not leak information. That handily
catches some basic and important qual pushdowns that would otherwise be lost.
This makes assumptions, at a distance, about the possible scan types and how
quals can be fitted to them. Specifically, this patch achieves its goals
because any indexable qual is trustworthy, and any non-indexable qual cannot
be pushed down further than the view's own quals. That seems to be true with
current plan types, but it feels fragile. I don't have a concrete idea for
improvement, though. Robert suggested another approach in
http://archives.postgresql.org/message-id/AANLkTimbN_6tYxReh5Rc7pmizT-VJB3xgp8CuHO0OAHC@mail.gmail.com
; might that approach avoid this hazard?
The part 2 patch in this series implements the planner restriction more likely
to yield performance regressions, so it introduces a mechanism for identifying
when to apply the restriction. This patch, however, applies its restriction
unconditionally. Since we will inevitably have a such mechanism before you
are done sealing the leaks in our view implementation, the restriction in this
patch should also use that mechanism. Therefore, I think we should review and
apply part 2 first, then update this patch to use its conditional behavior.
A few minor questions/comments on the implementation:
On Mon, Jun 06, 2011 at 01:37:11PM +0100, Kohei Kaigai wrote:
--- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c
+ else if (IsA(node, Query)) + { + depth += 2;
Why two?
--- a/src/test/regress/expected/select_views.out +++ b/src/test/regress/expected/select_views.out
+EXPLAIN SELECT * FROM your_credit WHERE f_leak(number,expired); + QUERY PLAN +-------------------------------------------------------------------------- + Seq Scan on credit_cards (cost=0.00..181.20 rows=1 width=96)
Use "EXPLAIN (COSTS OFF)" in regression tests. We do not put much effort into
the stability of exact cost values, and they do not matter for the purpose of
this test.
--- a/src/test/regress/sql/select_views.sql +++ b/src/test/regress/sql/select_views.sql @@ -8,3 +8,46 @@ SELECT * FROM street; SELECT name, #thepath FROM iexit ORDER BY 1, 2;SELECT * FROM toyemp WHERE name = 'sharon'; + +-- +-- Test for leaky-view problem +-- + +-- setups +SET client_min_messages TO 'warning'; + +DROP ROLE IF EXISTS alice; +DROP FUNCTION IF EXISTS f_leak(text); +DROP TABLE IF EXISTS credit_cards; + +RESET client_min_messages;
No need for this. The regression tests always start on a clean database.
+ +CREATE USER alice; +CREATE FUNCTION f_leak(text, text) + RETURNS bool LANGUAGE 'plpgsql' + AS 'begin raise notice ''% => %'', $1, $2; return true; end';
I ran this test case on master, and it did not reproduce the problem.
However, adding "COST 0.1" to this CREATE FUNCTION did yield the expected
problem behavior. I suggest adding that.
Thanks,
nm
Thanks for your reviewing,
2011/6/28 Noah Misch <noah@leadboat.com>:
I took a look at this patch. It's incredibly simple, which is great, and it
seems to achieve its goal.Suppose your query references two views owned by different roles. The quals
of those views will have the same depth. Is there a way for information to
leak from one view owner to another due to that?
Even if multiple subqueries were pulled-up and qualifiers got merged, user given
qualifiers shall have smaller depth value, so it will be always
launched after the
qualifiers originally used in the subqueries.
Of course, it is another topic in the case when the view is originally
defined with
leaky functions.
I like how you've assumed that the table owner trusts the operator class
functions of indexes on his table to not leak information. That handily
catches some basic and important qual pushdowns that would otherwise be lost.This makes assumptions, at a distance, about the possible scan types and how
quals can be fitted to them. Specifically, this patch achieves its goals
because any indexable qual is trustworthy, and any non-indexable qual cannot
be pushed down further than the view's own quals. That seems to be true with
current plan types, but it feels fragile. I don't have a concrete idea for
improvement, though. Robert suggested another approach in
http://archives.postgresql.org/message-id/AANLkTimbN_6tYxReh5Rc7pmizT-VJB3xgp8CuHO0OAHC@mail.gmail.com
; might that approach avoid this hazard?
The reason why we didn't adopt the idea to check privileges of underlying tables
is that PostgreSQL checks privileges on executor phase, not planner phase.
If we try to have a flag on pg_proc, it is a tough work to categolize trustworth
functions and non-trustworh ones from beginning, because we have more than
2000 of built-in functions.
So, it is reasonable assumption that index access methods does not leak
contents of tuples scanned, because only superuser can define them.
The part 2 patch in this series implements the planner restriction more likely
to yield performance regressions, so it introduces a mechanism for identifying
when to apply the restriction. This patch, however, applies its restriction
unconditionally. Since we will inevitably have a such mechanism before you
are done sealing the leaks in our view implementation, the restriction in this
patch should also use that mechanism. Therefore, I think we should review and
apply part 2 first, then update this patch to use its conditional behavior.
The reason why this patch always gives the depth higher priority is the matter
is relatively minor compared to the issue the part.2 patch tries to tackle.
That affects the selection of scan plan (IndexScan or SeqScan), so it
is significant
decision to be controllable. However, this issue is just on a particular scan.
In addition, implementation will become complex, if both of qualifiers pulled-up
from security barrier view and qualifiers pulled-up from regular views are mixed
within a single qualifier list.
A few minor questions/comments on the implementation:
On Mon, Jun 06, 2011 at 01:37:11PM +0100, Kohei Kaigai wrote:
--- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c+ else if (IsA(node, Query)) + { + depth += 2;Why two?
This patch is a groundwork for the upcoming row-level security feature.
In the case when the backend appends security policy functions, it should
be launched prior to user given qualifiers. So, I intends to give odd depth
numbers for these functions to have higher priorities to other one.
Of course, 1 might work well right now.
--- a/src/test/regress/expected/select_views.out +++ b/src/test/regress/expected/select_views.out+EXPLAIN SELECT * FROM your_credit WHERE f_leak(number,expired); + QUERY PLAN +-------------------------------------------------------------------------- + Seq Scan on credit_cards (cost=0.00..181.20 rows=1 width=96)Use "EXPLAIN (COSTS OFF)" in regression tests. We do not put much effort into
the stability of exact cost values, and they do not matter for the purpose of
this test.
OK, fixed.
--- a/src/test/regress/sql/select_views.sql +++ b/src/test/regress/sql/select_views.sql @@ -8,3 +8,46 @@ SELECT * FROM street; SELECT name, #thepath FROM iexit ORDER BY 1, 2;SELECT * FROM toyemp WHERE name = 'sharon'; + +-- +-- Test for leaky-view problem +-- + +-- setups +SET client_min_messages TO 'warning'; + +DROP ROLE IF EXISTS alice; +DROP FUNCTION IF EXISTS f_leak(text); +DROP TABLE IF EXISTS credit_cards; + +RESET client_min_messages;No need for this. The regression tests always start on a clean database.
Fixed.
+ +CREATE USER alice; +CREATE FUNCTION f_leak(text, text) + RETURNS bool LANGUAGE 'plpgsql' + AS 'begin raise notice ''% => %'', $1, $2; return true; end';I ran this test case on master, and it did not reproduce the problem.
However, adding "COST 0.1" to this CREATE FUNCTION did yield the expected
problem behavior. I suggest adding that.
Thanks, I might miss the point of regression test.
The attached patch is revised one on regression test.
Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
Attachments:
pgsql-v9.2-fix-leaky-view-part-1.v2.patchapplication/octet-stream; name=pgsql-v9.2-fix-leaky-view-part-1.v2.patchDownload
src/backend/nodes/copyfuncs.c | 8 ++
src/backend/nodes/equalfuncs.c | 8 ++
src/backend/nodes/outfuncs.c | 8 ++
src/backend/nodes/readfuncs.c | 8 ++
src/backend/optimizer/path/costsize.c | 17 ++++
src/backend/optimizer/plan/createplan.c | 13 +++-
src/backend/optimizer/plan/planner.c | 114 ++++++++++++++++++++++++++++
src/backend/optimizer/util/clauses.c | 24 ++++--
src/include/nodes/primnodes.h | 6 ++
src/include/nodes/relation.h | 1 +
src/test/regress/expected/select_views.out | 70 ++++++++++++++----
src/test/regress/sql/select_views.sql | 35 +++++++++
12 files changed, 289 insertions(+), 23 deletions(-)
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index c9133dd..1b1cf93 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -1188,6 +1188,7 @@ _copyFuncExpr(FuncExpr *from)
COPY_SCALAR_FIELD(inputcollid);
COPY_NODE_FIELD(args);
COPY_LOCATION_FIELD(location);
+ COPY_SCALAR_FIELD(depth);
return newnode;
}
@@ -1224,6 +1225,7 @@ _copyOpExpr(OpExpr *from)
COPY_SCALAR_FIELD(inputcollid);
COPY_NODE_FIELD(args);
COPY_LOCATION_FIELD(location);
+ COPY_SCALAR_FIELD(depth);
return newnode;
}
@@ -1244,6 +1246,7 @@ _copyDistinctExpr(DistinctExpr *from)
COPY_SCALAR_FIELD(inputcollid);
COPY_NODE_FIELD(args);
COPY_LOCATION_FIELD(location);
+ COPY_SCALAR_FIELD(depth);
return newnode;
}
@@ -1264,6 +1267,7 @@ _copyNullIfExpr(NullIfExpr *from)
COPY_SCALAR_FIELD(inputcollid);
COPY_NODE_FIELD(args);
COPY_LOCATION_FIELD(location);
+ COPY_SCALAR_FIELD(depth);
return newnode;
}
@@ -1282,6 +1286,7 @@ _copyScalarArrayOpExpr(ScalarArrayOpExpr *from)
COPY_SCALAR_FIELD(inputcollid);
COPY_NODE_FIELD(args);
COPY_LOCATION_FIELD(location);
+ COPY_SCALAR_FIELD(depth);
return newnode;
}
@@ -1422,6 +1427,7 @@ _copyCoerceViaIO(CoerceViaIO *from)
COPY_SCALAR_FIELD(resultcollid);
COPY_SCALAR_FIELD(coerceformat);
COPY_LOCATION_FIELD(location);
+ COPY_SCALAR_FIELD(depth);
return newnode;
}
@@ -1442,6 +1448,7 @@ _copyArrayCoerceExpr(ArrayCoerceExpr *from)
COPY_SCALAR_FIELD(isExplicit);
COPY_SCALAR_FIELD(coerceformat);
COPY_LOCATION_FIELD(location);
+ COPY_SCALAR_FIELD(depth);
return newnode;
}
@@ -1574,6 +1581,7 @@ _copyRowCompareExpr(RowCompareExpr *from)
COPY_NODE_FIELD(inputcollids);
COPY_NODE_FIELD(largs);
COPY_NODE_FIELD(rargs);
+ COPY_SCALAR_FIELD(depth);
return newnode;
}
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 3a0267c..71c4694 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -248,6 +248,7 @@ _equalFuncExpr(FuncExpr *a, FuncExpr *b)
COMPARE_SCALAR_FIELD(inputcollid);
COMPARE_NODE_FIELD(args);
COMPARE_LOCATION_FIELD(location);
+ COMPARE_SCALAR_FIELD(depth);
return true;
}
@@ -285,6 +286,7 @@ _equalOpExpr(OpExpr *a, OpExpr *b)
COMPARE_SCALAR_FIELD(inputcollid);
COMPARE_NODE_FIELD(args);
COMPARE_LOCATION_FIELD(location);
+ COMPARE_SCALAR_FIELD(depth);
return true;
}
@@ -311,6 +313,7 @@ _equalDistinctExpr(DistinctExpr *a, DistinctExpr *b)
COMPARE_SCALAR_FIELD(inputcollid);
COMPARE_NODE_FIELD(args);
COMPARE_LOCATION_FIELD(location);
+ COMPARE_SCALAR_FIELD(depth);
return true;
}
@@ -337,6 +340,7 @@ _equalNullIfExpr(NullIfExpr *a, NullIfExpr *b)
COMPARE_SCALAR_FIELD(inputcollid);
COMPARE_NODE_FIELD(args);
COMPARE_LOCATION_FIELD(location);
+ COMPARE_SCALAR_FIELD(depth);
return true;
}
@@ -361,6 +365,7 @@ _equalScalarArrayOpExpr(ScalarArrayOpExpr *a, ScalarArrayOpExpr *b)
COMPARE_SCALAR_FIELD(inputcollid);
COMPARE_NODE_FIELD(args);
COMPARE_LOCATION_FIELD(location);
+ COMPARE_SCALAR_FIELD(depth);
return true;
}
@@ -479,6 +484,7 @@ _equalCoerceViaIO(CoerceViaIO *a, CoerceViaIO *b)
return false;
COMPARE_LOCATION_FIELD(location);
+ COMPARE_SCALAR_FIELD(depth);
return true;
}
@@ -503,6 +509,7 @@ _equalArrayCoerceExpr(ArrayCoerceExpr *a, ArrayCoerceExpr *b)
return false;
COMPARE_LOCATION_FIELD(location);
+ COMPARE_SCALAR_FIELD(depth);
return true;
}
@@ -613,6 +620,7 @@ _equalRowCompareExpr(RowCompareExpr *a, RowCompareExpr *b)
COMPARE_NODE_FIELD(inputcollids);
COMPARE_NODE_FIELD(largs);
COMPARE_NODE_FIELD(rargs);
+ COMPARE_SCALAR_FIELD(depth);
return true;
}
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 681f5f8..f9a8aef 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -1004,6 +1004,7 @@ _outFuncExpr(StringInfo str, FuncExpr *node)
WRITE_OID_FIELD(inputcollid);
WRITE_NODE_FIELD(args);
WRITE_LOCATION_FIELD(location);
+ WRITE_INT_FIELD(depth);
}
static void
@@ -1030,6 +1031,7 @@ _outOpExpr(StringInfo str, OpExpr *node)
WRITE_OID_FIELD(inputcollid);
WRITE_NODE_FIELD(args);
WRITE_LOCATION_FIELD(location);
+ WRITE_INT_FIELD(depth);
}
static void
@@ -1045,6 +1047,7 @@ _outDistinctExpr(StringInfo str, DistinctExpr *node)
WRITE_OID_FIELD(inputcollid);
WRITE_NODE_FIELD(args);
WRITE_LOCATION_FIELD(location);
+ WRITE_INT_FIELD(depth);
}
static void
@@ -1060,6 +1063,7 @@ _outNullIfExpr(StringInfo str, NullIfExpr *node)
WRITE_OID_FIELD(inputcollid);
WRITE_NODE_FIELD(args);
WRITE_LOCATION_FIELD(location);
+ WRITE_INT_FIELD(depth);
}
static void
@@ -1073,6 +1077,7 @@ _outScalarArrayOpExpr(StringInfo str, ScalarArrayOpExpr *node)
WRITE_OID_FIELD(inputcollid);
WRITE_NODE_FIELD(args);
WRITE_LOCATION_FIELD(location);
+ WRITE_INT_FIELD(depth);
}
static void
@@ -1190,6 +1195,7 @@ _outCoerceViaIO(StringInfo str, CoerceViaIO *node)
WRITE_OID_FIELD(resultcollid);
WRITE_ENUM_FIELD(coerceformat, CoercionForm);
WRITE_LOCATION_FIELD(location);
+ WRITE_INT_FIELD(depth);
}
static void
@@ -1205,6 +1211,7 @@ _outArrayCoerceExpr(StringInfo str, ArrayCoerceExpr *node)
WRITE_BOOL_FIELD(isExplicit);
WRITE_ENUM_FIELD(coerceformat, CoercionForm);
WRITE_LOCATION_FIELD(location);
+ WRITE_INT_FIELD(depth);
}
static void
@@ -1297,6 +1304,7 @@ _outRowCompareExpr(StringInfo str, RowCompareExpr *node)
WRITE_NODE_FIELD(inputcollids);
WRITE_NODE_FIELD(largs);
WRITE_NODE_FIELD(rargs);
+ WRITE_INT_FIELD(depth);
}
static void
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 29a0e8f..c4f934d 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -541,6 +541,7 @@ _readFuncExpr(void)
READ_OID_FIELD(inputcollid);
READ_NODE_FIELD(args);
READ_LOCATION_FIELD(location);
+ READ_INT_FIELD(depth);
READ_DONE();
}
@@ -588,6 +589,7 @@ _readOpExpr(void)
READ_OID_FIELD(inputcollid);
READ_NODE_FIELD(args);
READ_LOCATION_FIELD(location);
+ READ_INT_FIELD(depth);
READ_DONE();
}
@@ -619,6 +621,7 @@ _readDistinctExpr(void)
READ_OID_FIELD(inputcollid);
READ_NODE_FIELD(args);
READ_LOCATION_FIELD(location);
+ READ_INT_FIELD(depth);
READ_DONE();
}
@@ -650,6 +653,7 @@ _readNullIfExpr(void)
READ_OID_FIELD(inputcollid);
READ_NODE_FIELD(args);
READ_LOCATION_FIELD(location);
+ READ_INT_FIELD(depth);
READ_DONE();
}
@@ -679,6 +683,7 @@ _readScalarArrayOpExpr(void)
READ_OID_FIELD(inputcollid);
READ_NODE_FIELD(args);
READ_LOCATION_FIELD(location);
+ READ_INT_FIELD(depth);
READ_DONE();
}
@@ -794,6 +799,7 @@ _readCoerceViaIO(void)
READ_OID_FIELD(resultcollid);
READ_ENUM_FIELD(coerceformat, CoercionForm);
READ_LOCATION_FIELD(location);
+ READ_INT_FIELD(depth);
READ_DONE();
}
@@ -814,6 +820,7 @@ _readArrayCoerceExpr(void)
READ_BOOL_FIELD(isExplicit);
READ_ENUM_FIELD(coerceformat, CoercionForm);
READ_LOCATION_FIELD(location);
+ READ_INT_FIELD(depth);
READ_DONE();
}
@@ -946,6 +953,7 @@ _readRowCompareExpr(void)
READ_NODE_FIELD(inputcollids);
READ_NODE_FIELD(largs);
READ_NODE_FIELD(rargs);
+ READ_INT_FIELD(depth);
READ_DONE();
}
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index bb38768..0302ad2 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -2593,6 +2593,7 @@ cost_qual_eval(QualCost *cost, List *quals, PlannerInfo *root)
context.root = root;
context.total.startup = 0;
context.total.per_tuple = 0;
+ context.total.depth = 0;
/* We don't charge any cost for the implicit ANDing at top level ... */
@@ -2618,6 +2619,7 @@ cost_qual_eval_node(QualCost *cost, Node *qual, PlannerInfo *root)
context.root = root;
context.total.startup = 0;
context.total.per_tuple = 0;
+ context.total.depth = 0;
cost_qual_eval_walker(qual, &context);
@@ -2647,6 +2649,7 @@ cost_qual_eval_walker(Node *node, cost_qual_eval_context *context)
locContext.root = context->root;
locContext.total.startup = 0;
locContext.total.per_tuple = 0;
+ locContext.total.depth = 0;
/*
* For an OR clause, recurse into the marked-up tree so that we
@@ -2671,6 +2674,8 @@ cost_qual_eval_walker(Node *node, cost_qual_eval_context *context)
}
context->total.startup += rinfo->eval_cost.startup;
context->total.per_tuple += rinfo->eval_cost.per_tuple;
+ if (rinfo->eval_cost.depth > context->total.depth)
+ context->total.depth = rinfo->eval_cost.depth;
/* do NOT recurse into children */
return false;
}
@@ -2694,6 +2699,8 @@ cost_qual_eval_walker(Node *node, cost_qual_eval_context *context)
{
context->total.per_tuple +=
get_func_cost(((FuncExpr *) node)->funcid) * cpu_operator_cost;
+ if (((FuncExpr *)node)->depth > context->total.depth)
+ context->total.depth = ((FuncExpr *)node)->depth;
}
else if (IsA(node, OpExpr) ||
IsA(node, DistinctExpr) ||
@@ -2703,6 +2710,8 @@ cost_qual_eval_walker(Node *node, cost_qual_eval_context *context)
set_opfuncid((OpExpr *) node);
context->total.per_tuple +=
get_func_cost(((OpExpr *) node)->opfuncid) * cpu_operator_cost;
+ if (((OpExpr *)node)->depth > context->total.depth)
+ context->total.depth = ((OpExpr *)node)->depth;
}
else if (IsA(node, ScalarArrayOpExpr))
{
@@ -2716,6 +2725,8 @@ cost_qual_eval_walker(Node *node, cost_qual_eval_context *context)
set_sa_opfuncid(saop);
context->total.per_tuple += get_func_cost(saop->opfuncid) *
cpu_operator_cost * estimate_array_length(arraynode) * 0.5;
+ if (saop->depth > context->total.depth)
+ context->total.depth = saop->depth;
}
else if (IsA(node, Aggref) ||
IsA(node, WindowFunc))
@@ -2746,6 +2757,8 @@ cost_qual_eval_walker(Node *node, cost_qual_eval_context *context)
getTypeOutputInfo(exprType((Node *) iocoerce->arg),
&iofunc, &typisvarlena);
context->total.per_tuple += get_func_cost(iofunc) * cpu_operator_cost;
+ if (iocoerce->depth > context->total.depth)
+ context->total.depth = iocoerce->depth;
}
else if (IsA(node, ArrayCoerceExpr))
{
@@ -2755,6 +2768,8 @@ cost_qual_eval_walker(Node *node, cost_qual_eval_context *context)
if (OidIsValid(acoerce->elemfuncid))
context->total.per_tuple += get_func_cost(acoerce->elemfuncid) *
cpu_operator_cost * estimate_array_length(arraynode);
+ if (acoerce->depth > context->total.depth)
+ context->total.depth = acoerce->depth;
}
else if (IsA(node, RowCompareExpr))
{
@@ -2769,6 +2784,8 @@ cost_qual_eval_walker(Node *node, cost_qual_eval_context *context)
context->total.per_tuple += get_func_cost(get_opcode(opid)) *
cpu_operator_cost;
}
+ if (rcexpr->depth > context->total.depth)
+ context->total.depth = rcexpr->depth;
}
else if (IsA(node, CurrentOfExpr))
{
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index e4ccf5c..3414b4c 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -2704,6 +2704,7 @@ order_qual_clauses(PlannerInfo *root, List *clauses)
{
Node *clause;
Cost cost;
+ int depth;
} QualItem;
int nitems = list_length(clauses);
QualItem *items;
@@ -2729,6 +2730,7 @@ order_qual_clauses(PlannerInfo *root, List *clauses)
cost_qual_eval_node(&qcost, clause, root);
items[i].clause = clause;
items[i].cost = qcost.per_tuple;
+ items[i].depth = qcost.depth;
i++;
}
@@ -2745,7 +2747,16 @@ order_qual_clauses(PlannerInfo *root, List *clauses)
/* insert newitem into the already-sorted subarray */
for (j = i; j > 0; j--)
{
- if (newitem.cost >= items[j - 1].cost)
+ /*
+ * Higher priority shall be given to the items originated from
+ * deeper nest level. If same level, it shall be given to the
+ * items with smaller estimated cost.
+ * Such kind of consideration is needed to prevent leaky-view
+ * problem.
+ */
+ if (newitem.depth < items[j - 1].depth ||
+ (newitem.depth == items[j - 1].depth &&
+ newitem.cost >= items[j - 1].cost))
break;
items[j] = items[j - 1];
}
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 9aafc8a..d58d789 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -33,6 +33,7 @@
#include "optimizer/subselect.h"
#include "optimizer/tlist.h"
#include "optimizer/var.h"
+#include "nodes/nodeFuncs.h"
#ifdef OPTIMIZER_DEBUG
#include "nodes/print.h"
#endif
@@ -103,6 +104,7 @@ static void get_column_info_for_window(PlannerInfo *root, WindowClause *wc,
int *ordNumCols,
AttrNumber **ordColIdx,
Oid **ordOperators);
+static void mark_qualifiers_depth(Query *query);
/*****************************************************************************
@@ -148,6 +150,12 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams)
cursorOptions |= ((DeclareCursorStmt *) parse->utilityStmt)->options;
/*
+ * Mark qualifiers its original depth to prevent reversal of orders
+ * on evaluation of WHERE clause during relation scanns.
+ */
+ mark_qualifiers_depth(parse);
+
+ /*
* Set up global state for this planner invocation. This data is needed
* across all levels of sub-Query that might exist in the given command,
* so we keep it in a separate struct that's linked to by each per-Query
@@ -2993,6 +3001,112 @@ get_column_info_for_window(PlannerInfo *root, WindowClause *wc, List *tlist,
}
}
+/*
+ * mark_qualifiers_depth
+ *
+ * It marks depth field of the each expression nodes that eventually
+ * invokes functions, to track the original nest-level. On the evaluation
+ * of qualifiers within WHERE or JOIN ... ON clauses during relation scans,
+ * these items shall be reordered according to the nest-level and estimated
+ * cost.
+ * The optimizer may pull-up simple sub-queries or join clause, and
+ * qualifiers to filter out tuples shall be mixed with ones in upper-
+ * level. Thus, we need to track the original nest-level of qualifiers
+ * to prevent reverse of order in evaluation, because some of qualifiers
+ * can have side-effects that allows to leak supplied argument to outside.
+ * It can be abused to break row-level security using a user defined function
+ * with very small estimated cost, so nest level of qualifiers originated
+ * from is used as a criteria, rather than estimated cost, to decide order
+ * to evaluate qualifiers.
+ */
+static bool
+mark_qualifiers_depth_walker(Node *node, void *context)
+{
+ int depth = *((int *)(context));
+
+ if (node == NULL)
+ return false;
+ if (IsA(node, FuncExpr))
+ {
+ FuncExpr *exp = (FuncExpr *)node;
+
+ exp->depth = depth;
+
+ return false;
+ }
+ else if (IsA(node, OpExpr))
+ {
+ OpExpr *exp = (OpExpr *)node;
+
+ exp->depth = depth;
+
+ return false;
+ }
+ else if (IsA(node, DistinctExpr))
+ {
+ DistinctExpr *exp = (DistinctExpr *)node;
+
+ exp->depth = depth;
+
+ return false;
+ }
+ else if (IsA(node, ScalarArrayOpExpr))
+ {
+ ScalarArrayOpExpr *exp = (ScalarArrayOpExpr *)node;
+
+ exp->depth = depth;
+
+ return false;
+ }
+ else if (IsA(node, CoerceViaIO))
+ {
+ CoerceViaIO *exp = (CoerceViaIO *)node;
+
+ exp->depth = depth;
+
+ return false;
+ }
+ else if (IsA(node, ArrayCoerceExpr))
+ {
+ ArrayCoerceExpr *exp = (ArrayCoerceExpr *)node;
+
+ exp->depth = depth;
+
+ return false;
+ }
+ else if (IsA(node, NullIfExpr))
+ {
+ NullIfExpr *exp = (NullIfExpr *)node;
+
+ exp->depth = depth;
+
+ return false;
+ }
+ else if (IsA(node, RowCompareExpr))
+ {
+ RowCompareExpr *exp = (RowCompareExpr *)node;
+
+ exp->depth = depth;
+
+ return false;
+ }
+ else if (IsA(node, Query))
+ {
+ depth += 2;
+
+ query_tree_walker((Query *)node, mark_qualifiers_depth_walker, &depth, 0);
+ return false;
+ }
+ return expression_tree_walker(node, mark_qualifiers_depth_walker, context);
+}
+
+static void
+mark_qualifiers_depth(Query *query)
+{
+ int depth = 0;
+
+ query_tree_walker(query, mark_qualifiers_depth_walker, &depth, 0);
+}
/*
* expression_planner
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index be0935d..95c07aa 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -111,6 +111,7 @@ static Expr *simplify_function(Expr *oldexpr, Oid funcid,
Oid input_collid, List **args,
bool has_named_args,
bool allow_inline,
+ int depth,
eval_const_expressions_context *context);
static List *reorder_function_arguments(List *args, Oid result_type,
HeapTuple func_tuple,
@@ -123,7 +124,7 @@ static void recheck_cast_function_args(List *args, Oid result_type,
HeapTuple func_tuple);
static Expr *evaluate_function(Oid funcid, Oid result_type, int32 result_typmod,
Oid result_collid, Oid input_collid, List *args,
- HeapTuple func_tuple,
+ HeapTuple func_tuple, int depth,
eval_const_expressions_context *context);
static Expr *inline_function(Oid funcid, Oid result_type, Oid result_collid,
Oid input_collid, List *args,
@@ -2229,7 +2230,7 @@ eval_const_expressions_mutator(Node *node,
expr->funccollid,
expr->inputcollid,
&args,
- has_named_args, true, context);
+ has_named_args, true, expr->depth, context);
if (simple) /* successfully simplified it */
return (Node *) simple;
@@ -2248,6 +2249,7 @@ eval_const_expressions_mutator(Node *node,
newexpr->inputcollid = expr->inputcollid;
newexpr->args = args;
newexpr->location = expr->location;
+ newexpr->depth = expr->depth;
return (Node *) newexpr;
}
if (IsA(node, OpExpr))
@@ -2282,7 +2284,7 @@ eval_const_expressions_mutator(Node *node,
expr->opcollid,
expr->inputcollid,
&args,
- false, true, context);
+ false, true, expr->depth, context);
if (simple) /* successfully simplified it */
return (Node *) simple;
@@ -2313,6 +2315,7 @@ eval_const_expressions_mutator(Node *node,
newexpr->inputcollid = expr->inputcollid;
newexpr->args = args;
newexpr->location = expr->location;
+ newexpr->depth = expr->depth;
return (Node *) newexpr;
}
if (IsA(node, DistinctExpr))
@@ -2380,7 +2383,7 @@ eval_const_expressions_mutator(Node *node,
expr->opcollid,
expr->inputcollid,
&args,
- false, false, context);
+ false, false, expr->depth, context);
if (simple) /* successfully simplified it */
{
/*
@@ -2410,6 +2413,7 @@ eval_const_expressions_mutator(Node *node,
newexpr->inputcollid = expr->inputcollid;
newexpr->args = args;
newexpr->location = expr->location;
+ newexpr->depth = expr->depth;
return (Node *) newexpr;
}
if (IsA(node, BoolExpr))
@@ -2570,7 +2574,7 @@ eval_const_expressions_mutator(Node *node,
InvalidOid,
InvalidOid,
&args,
- false, true, context);
+ false, true, expr->depth, context);
if (simple) /* successfully simplified output fn */
{
/*
@@ -2591,7 +2595,7 @@ eval_const_expressions_mutator(Node *node,
expr->resultcollid,
InvalidOid,
&args,
- false, true, context);
+ false, true, expr->depth, context);
if (simple) /* successfully simplified input fn */
return (Node *) simple;
}
@@ -2607,6 +2611,7 @@ eval_const_expressions_mutator(Node *node,
newexpr->resultcollid = expr->resultcollid;
newexpr->coerceformat = expr->coerceformat;
newexpr->location = expr->location;
+ newexpr->depth = expr->depth;
return (Node *) newexpr;
}
if (IsA(node, ArrayCoerceExpr))
@@ -2631,6 +2636,7 @@ eval_const_expressions_mutator(Node *node,
newexpr->isExplicit = expr->isExplicit;
newexpr->coerceformat = expr->coerceformat;
newexpr->location = expr->location;
+ newexpr->depth = expr->depth;
/*
* If constant argument and it's a binary-coercible or immutable
@@ -3447,6 +3453,7 @@ simplify_function(Expr *oldexpr, Oid funcid,
Oid input_collid, List **args,
bool has_named_args,
bool allow_inline,
+ int depth,
eval_const_expressions_context *context)
{
HeapTuple func_tuple;
@@ -3477,7 +3484,7 @@ simplify_function(Expr *oldexpr, Oid funcid,
newexpr = evaluate_function(funcid, result_type, result_typmod,
result_collid, input_collid, *args,
- func_tuple, context);
+ func_tuple, depth, context);
/*
* Some functions calls can be simplified at plan time based on properties
@@ -3764,7 +3771,7 @@ recheck_cast_function_args(List *args, Oid result_type, HeapTuple func_tuple)
static Expr *
evaluate_function(Oid funcid, Oid result_type, int32 result_typmod,
Oid result_collid, Oid input_collid, List *args,
- HeapTuple func_tuple,
+ HeapTuple func_tuple, int depth,
eval_const_expressions_context *context)
{
Form_pg_proc funcform = (Form_pg_proc) GETSTRUCT(func_tuple);
@@ -3850,6 +3857,7 @@ evaluate_function(Oid funcid, Oid result_type, int32 result_typmod,
newexpr->inputcollid = input_collid;
newexpr->args = args;
newexpr->location = -1;
+ newexpr->depth = depth;
return evaluate_expr((Expr *) newexpr, result_type, result_typmod,
result_collid);
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index f1e20ef..b626386 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -335,6 +335,7 @@ typedef struct FuncExpr
Oid inputcollid; /* OID of collation that function should use */
List *args; /* arguments to the function */
int location; /* token location, or -1 if unknown */
+ int depth; /* depth of clause in the original query */
} FuncExpr;
/*
@@ -380,6 +381,7 @@ typedef struct OpExpr
Oid inputcollid; /* OID of collation that operator should use */
List *args; /* arguments to the operator (1 or 2) */
int location; /* token location, or -1 if unknown */
+ int depth; /* depth of clause in the original query */
} OpExpr;
/*
@@ -421,6 +423,7 @@ typedef struct ScalarArrayOpExpr
Oid inputcollid; /* OID of collation that operator should use */
List *args; /* the scalar and array operands */
int location; /* token location, or -1 if unknown */
+ int depth; /* depth of clause in the original query */
} ScalarArrayOpExpr;
/*
@@ -685,6 +688,7 @@ typedef struct CoerceViaIO
Oid resultcollid; /* OID of collation, or InvalidOid if none */
CoercionForm coerceformat; /* how to display this node */
int location; /* token location, or -1 if unknown */
+ int depth; /* depth of clause in the original query */
} CoerceViaIO;
/* ----------------
@@ -710,6 +714,7 @@ typedef struct ArrayCoerceExpr
bool isExplicit; /* conversion semantics flag to pass to func */
CoercionForm coerceformat; /* how to display this node */
int location; /* token location, or -1 if unknown */
+ int depth; /* depth of clause in the original query */
} ArrayCoerceExpr;
/* ----------------
@@ -901,6 +906,7 @@ typedef struct RowCompareExpr
List *inputcollids; /* OID list of collations for comparisons */
List *largs; /* the left-hand input arguments */
List *rargs; /* the right-hand input arguments */
+ int depth; /* depth of clause in the original query */
} RowCompareExpr;
/*
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index f659269..250191b 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -44,6 +44,7 @@ typedef struct QualCost
{
Cost startup; /* one-time cost */
Cost per_tuple; /* per-evaluation cost */
+ int depth; /* depth of qual in the original query */
} QualCost;
/*
diff --git a/src/test/regress/expected/select_views.out b/src/test/regress/expected/select_views.out
index 6cd317c..466689f 100644
--- a/src/test/regress/expected/select_views.out
+++ b/src/test/regress/expected/select_views.out
@@ -467,6 +467,20 @@ SELECT name, #thepath FROM iexit ORDER BY 1, 2;
I- 580 | 21
I- 580 | 22
I- 580 | 22
+ I- 580/I-680 Ramp | 2
+ I- 580/I-680 Ramp | 2
+ I- 580/I-680 Ramp | 2
+ I- 580/I-680 Ramp | 2
+ I- 580/I-680 Ramp | 2
+ I- 580/I-680 Ramp | 2
+ I- 580/I-680 Ramp | 4
+ I- 580/I-680 Ramp | 4
+ I- 580/I-680 Ramp | 4
+ I- 580/I-680 Ramp | 4
+ I- 580/I-680 Ramp | 5
+ I- 580/I-680 Ramp | 6
+ I- 580/I-680 Ramp | 6
+ I- 580/I-680 Ramp | 6
I- 580 Ramp | 2
I- 580 Ramp | 2
I- 580 Ramp | 2
@@ -717,20 +731,6 @@ SELECT name, #thepath FROM iexit ORDER BY 1, 2;
I- 580 Ramp | 8
I- 580 Ramp | 8
I- 580 Ramp | 8
- I- 580/I-680 Ramp | 2
- I- 580/I-680 Ramp | 2
- I- 580/I-680 Ramp | 2
- I- 580/I-680 Ramp | 2
- I- 580/I-680 Ramp | 2
- I- 580/I-680 Ramp | 2
- I- 580/I-680 Ramp | 4
- I- 580/I-680 Ramp | 4
- I- 580/I-680 Ramp | 4
- I- 580/I-680 Ramp | 4
- I- 580/I-680 Ramp | 5
- I- 580/I-680 Ramp | 6
- I- 580/I-680 Ramp | 6
- I- 580/I-680 Ramp | 6
I- 680 | 2
I- 680 | 2
I- 680 | 2
@@ -1247,3 +1247,45 @@ SELECT * FROM toyemp WHERE name = 'sharon';
sharon | 25 | (15,12) | 12000
(1 row)
+--
+-- Test for leaky-view problem
+--
+CREATE USER alice;
+CREATE FUNCTION f_leak(text, text)
+ RETURNS bool LANGUAGE 'plpgsql'
+ COST 0.00000001
+ AS 'begin raise notice ''% => %'', $1, $2; return true; end';
+CREATE TABLE credit_cards (
+ name text,
+ number text,
+ expired text
+);
+INSERT INTO credit_cards VALUES ('alice', '1111-2222-3333-4444', 'Aug-2012'),
+ ('bob', '5555-6666-7777-8888', 'Nov-2016'),
+ ('eve', '9801-2345-6789-0123', 'Jan-2018');
+CREATE VIEW your_credit AS
+ SELECT * FROM credit_cards WHERE name = getpgusername();
+GRANT SELECT ON your_credit TO public;
+-- run leaky view
+SET SESSION AUTHORIZATION alice;
+SELECT * FROM your_credit WHERE f_leak(number,expired);
+NOTICE: 1111-2222-3333-4444 => Aug-2012
+ name | number | expired
+-------+---------------------+----------
+ alice | 1111-2222-3333-4444 | Aug-2012
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM your_credit WHERE f_leak(number,expired);
+ QUERY PLAN
+--------------------------------------------------------------------------
+ Seq Scan on credit_cards
+ Filter: ((name = (getpgusername())::text) AND f_leak(number, expired))
+(2 rows)
+
+\c -
+-- cleanups
+DROP ROLE IF EXISTS alice;
+DROP FUNCTION IF EXISTS f_leak(text);
+NOTICE: function f_leak(text) does not exist, skipping
+DROP TABLE IF EXISTS credit_cards CASCADE;
+NOTICE: drop cascades to view your_credit
diff --git a/src/test/regress/sql/select_views.sql b/src/test/regress/sql/select_views.sql
index 14f1be8..8ed1512 100644
--- a/src/test/regress/sql/select_views.sql
+++ b/src/test/regress/sql/select_views.sql
@@ -8,3 +8,38 @@ SELECT * FROM street;
SELECT name, #thepath FROM iexit ORDER BY 1, 2;
SELECT * FROM toyemp WHERE name = 'sharon';
+
+--
+-- Test for leaky-view problem
+--
+
+CREATE USER alice;
+CREATE FUNCTION f_leak(text, text)
+ RETURNS bool LANGUAGE 'plpgsql'
+ COST 0.00000001
+ AS 'begin raise notice ''% => %'', $1, $2; return true; end';
+CREATE TABLE credit_cards (
+ name text,
+ number text,
+ expired text
+);
+
+INSERT INTO credit_cards VALUES ('alice', '1111-2222-3333-4444', 'Aug-2012'),
+ ('bob', '5555-6666-7777-8888', 'Nov-2016'),
+ ('eve', '9801-2345-6789-0123', 'Jan-2018');
+CREATE VIEW your_credit AS
+ SELECT * FROM credit_cards WHERE name = getpgusername();
+
+GRANT SELECT ON your_credit TO public;
+-- run leaky view
+SET SESSION AUTHORIZATION alice;
+
+SELECT * FROM your_credit WHERE f_leak(number,expired);
+EXPLAIN (COSTS OFF) SELECT * FROM your_credit WHERE f_leak(number,expired);
+
+\c -
+-- cleanups
+DROP ROLE IF EXISTS alice;
+
+DROP FUNCTION IF EXISTS f_leak(text);
+DROP TABLE IF EXISTS credit_cards CASCADE;
On Tue, Jun 28, 2011 at 10:11:59PM +0200, Kohei KaiGai wrote:
2011/6/28 Noah Misch <noah@leadboat.com>:
Suppose your query references two views owned by different roles. ?The quals
of those views will have the same depth. ?Is there a way for information to
leak from one view owner to another due to that?Even if multiple subqueries were pulled-up and qualifiers got merged, user given
qualifiers shall have smaller depth value, so it will be always
launched after the
qualifiers originally used in the subqueries.Of course, it is another topic in the case when the view is originally
defined with
leaky functions.
Right. I was thinking of a pair of quals, one in each of two view definitions.
The views are mutually-untrusting. Something like this:
CREATE VIEW a AS SELECT * FROM ta WHERE ac = 5;
ALTER VIEW a OWNER TO alice;
CREATE VIEW b AS SELECT * FROM tb WHERE bc = 6;
ALTER VIEW b OWNER TO bob;
SELECT * FROM a, b;
Both the ac=5 and the bc=6 quals do run at the same depth despite enforcing
security for different principals. I can't think of a way that one view owner
could use this situation to subvert the security of the other view owner, but I
wanted to throw it out.
This makes assumptions, at a distance, about the possible scan types and how
quals can be fitted to them. ?Specifically, this patch achieves its goals
because any indexable qual is trustworthy, and any non-indexable qual cannot
be pushed down further than the view's own quals. ?That seems to be true with
current plan types, but it feels fragile. ?I don't have a concrete idea for
improvement, though. ?Robert suggested another approach in
http://archives.postgresql.org/message-id/AANLkTimbN_6tYxReh5Rc7pmizT-VJB3xgp8CuHO0OAHC@mail.gmail.com
; might that approach avoid this hazard?The reason why we didn't adopt the idea to check privileges of underlying tables
is that PostgreSQL checks privileges on executor phase, not planner phase.If we try to have a flag on pg_proc, it is a tough work to categolize trustworth
functions and non-trustworh ones from beginning, because we have more than
2000 of built-in functions.
So, it is reasonable assumption that index access methods does not leak
contents of tuples scanned, because only superuser can define them.
I was referring to this paragraph:
On the technical side, I am pretty doubtful that the approach of adding a
nestlevel to FuncExpr and RelOptInfo is the right way to go. I believe we
have existing code (to handle left joins) that prevents quals from being
pushed down too far by fudging the set of relations that are supposedly needed
to evaluate the qual. I suspect a similar approach would work here.
The part 2 patch in this series implements the planner restriction more likely
to yield performance regressions, so it introduces a mechanism for identifying
when to apply the restriction. ?This patch, however, applies its restriction
unconditionally. ?Since we will inevitably have a such mechanism before you
are done sealing the leaks in our view implementation, the restriction in this
patch should also use that mechanism. ?Therefore, I think we should review and
apply part 2 first, then update this patch to use its conditional behavior.The reason why this patch always gives the depth higher priority is the matter
is relatively minor compared to the issue the part.2 patch tries to tackle.
That affects the selection of scan plan (IndexScan or SeqScan), so it
is significant
decision to be controllable. However, this issue is just on a particular scan.
True. The lost optimization opportunity is relatively minor, but perhaps not
absolutely minor. It would be one thing if we could otherwise get away without
designing any mechanism for applying these restrictions conditionally. However,
since you have implemented the conditional behavior elsewhere, it would be nice
to apply it here.
In addition, implementation will become complex, if both of qualifiers pulled-up
from security barrier view and qualifiers pulled-up from regular views are mixed
within a single qualifier list.
I only scanned the part 2 patch, but isn't the bookkeeping already happening for
its purposes? How much more complexity would we get to apply the same strategy
to the behavior of this patch?
On Mon, Jun 06, 2011 at 01:37:11PM +0100, Kohei Kaigai wrote:
--- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c+ ? ? else if (IsA(node, Query)) + ? ? { + ? ? ? ? ? ? depth += 2;Why two?
This patch is a groundwork for the upcoming row-level security feature.
In the case when the backend appends security policy functions, it should
be launched prior to user given qualifiers. So, I intends to give odd depth
numbers for these functions to have higher priorities to other one.
Of course, 1 might work well right now.
I'd say it should either be 1 until such time as that's needed, or it needs a
comment noting why it's 2.
Thanks,
nm
2011/6/28 Noah Misch <noah@leadboat.com>:
On Tue, Jun 28, 2011 at 10:11:59PM +0200, Kohei KaiGai wrote:
2011/6/28 Noah Misch <noah@leadboat.com>:
Suppose your query references two views owned by different roles. ?The quals
of those views will have the same depth. ?Is there a way for information to
leak from one view owner to another due to that?Even if multiple subqueries were pulled-up and qualifiers got merged, user given
qualifiers shall have smaller depth value, so it will be always
launched after the
qualifiers originally used in the subqueries.Of course, it is another topic in the case when the view is originally
defined with
leaky functions.Right. I was thinking of a pair of quals, one in each of two view definitions.
The views are mutually-untrusting. Something like this:CREATE VIEW a AS SELECT * FROM ta WHERE ac = 5;
ALTER VIEW a OWNER TO alice;
CREATE VIEW b AS SELECT * FROM tb WHERE bc = 6;
ALTER VIEW b OWNER TO bob;
SELECT * FROM a, b;Both the ac=5 and the bc=6 quals do run at the same depth despite enforcing
security for different principals. I can't think of a way that one view owner
could use this situation to subvert the security of the other view owner, but I
wanted to throw it out.
Even if view owner set a trap in his view, we have no way to reference variables
come from outside of the view. In above example, even if I added f_leak() into
the definition of VIEW A, we cannot give argument to reference VIEW B.
I was referring to this paragraph:
On the technical side, I am pretty doubtful that the approach of adding a
nestlevel to FuncExpr and RelOptInfo is the right way to go. I believe we
have existing code (to handle left joins) that prevents quals from being
pushed down too far by fudging the set of relations that are supposedly needed
to evaluate the qual. I suspect a similar approach would work here.
It seems to me the later half of this paragraph is talking about the problem of
unexpected qualifier pushing-down over the security barrier; I'm trying to solve
the problem with the part.2 patch.
The scenario the part.1 patch tries to solve is order to launch qualifiers, not
unexpected pushing-down.
As long as we focus on the ordering problem, it is reasonable approach to
track original nestlevel to enforce to launch qualifier come from deeper level
earlier. Because it makes performance damages, if we prevent pull-up
subqueries come from security view.
For example, if we cannot pull-up this subquery, we will need to scan the
relation twice.
SELECT * FROM (SELECT * FROM tbl WHERE f_policy(x)) WHERE f_leak(y);
Normally, it should be pulled-up into the following form:
SELECT * FROM tbl WHERE f_policy(x) AND f_leak(y);
In addition, implementation will become complex, if both of qualifiers pulled-up
from security barrier view and qualifiers pulled-up from regular views are mixed
within a single qualifier list.I only scanned the part 2 patch, but isn't the bookkeeping already happening for
its purposes? How much more complexity would we get to apply the same strategy
to the behavior of this patch?
If conditional, what criteria we should have to reorder the quelifier?
The current patch checks the depth at first, then it checks cost if same deptn.
It is quite simple rule. I have no idea of the criteria to order the
mixed qualifier
come from security-barrier views and regular views.
On Mon, Jun 06, 2011 at 01:37:11PM +0100, Kohei Kaigai wrote:
--- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c+ ? ? else if (IsA(node, Query)) + ? ? { + ? ? ? ? ? ? depth += 2;Why two?
This patch is a groundwork for the upcoming row-level security feature.
In the case when the backend appends security policy functions, it should
be launched prior to user given qualifiers. So, I intends to give odd depth
numbers for these functions to have higher priorities to other one.
Of course, 1 might work well right now.I'd say it should either be 1 until such time as that's needed, or it needs a
comment noting why it's 2.
OK, I'll add comment to introduce why the depth is incremented by 2.
Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
On Wed, Jun 29, 2011 at 05:05:22PM +0100, Kohei KaiGai wrote:
2011/6/28 Noah Misch <noah@leadboat.com>:
On Tue, Jun 28, 2011 at 10:11:59PM +0200, Kohei KaiGai wrote:
CREATE VIEW a AS SELECT * FROM ta WHERE ac = 5;
ALTER VIEW a OWNER TO alice;
CREATE VIEW b AS SELECT * FROM tb WHERE bc = 6;
ALTER VIEW b OWNER TO bob;
SELECT * FROM a, b;Both the ac=5 and the bc=6 quals do run at the same depth despite enforcing
security for different principals. ?I can't think of a way that one view owner
could use this situation to subvert the security of the other view owner, but I
wanted to throw it out.Even if view owner set a trap in his view, we have no way to reference variables
come from outside of the view. In above example, even if I added f_leak() into
the definition of VIEW A, we cannot give argument to reference VIEW B.
Good point. Yes, it should be rigorously safe on that account.
I was referring to this paragraph:
?On the technical side, I am pretty doubtful that the approach of adding a
?nestlevel to FuncExpr and RelOptInfo is the right way to go. ?I believe we
?have existing code (to handle left joins) that prevents quals from being
?pushed down too far by fudging the set of relations that are supposedly needed
?to evaluate the qual. ?I suspect a similar approach would work here.It seems to me the later half of this paragraph is talking about the problem of
unexpected qualifier pushing-down over the security barrier; I'm trying to solve
the problem with the part.2 patch.
The scenario the part.1 patch tries to solve is order to launch qualifiers, not
unexpected pushing-down.
Okay, you're probably correct that it wasn't referring to the topic at hand.
I'm still suspicious of the silent assumption about how quals can be assigned
to plan nodes, but I don't have any concrete ideas for avoiding that.
In addition, implementation will become complex, if both of qualifiers pulled-up
from security barrier view and qualifiers pulled-up from regular views are mixed
within a single qualifier list.I only scanned the part 2 patch, but isn't the bookkeeping already happening for
its purposes? ?How much more complexity would we get to apply the same strategy
to the behavior of this patch?If conditional, what criteria we should have to reorder the quelifier?
The current patch checks the depth at first, then it checks cost if same deptn.
It is quite simple rule. I have no idea of the criteria to order the
mixed qualifier
come from security-barrier views and regular views.
Let's see. Every qual list will have some depth d such that all quals having
depth >= d are security-relevant, and all others are not security-relevant.
(This does not hold for all means of identifying security-relevant quals, but
it does hold for the CREATE SECURITY VIEW/reloptions strategy proposed in your
part 2 patch.) Suppose you track whether each Query node represents a
security view, then only increment the qualifier depth for such Query nodes,
rather than all Query nodes. The tracked depth then becomes a security
partition depth. Keep the actual sorting algorithm the same. (Disclaimer: I
haven't been thinking about this nearly as long as you have, so I may be
missing something relatively obvious.)
As it stands, the patch badly damages the performance of this example:
CREATE FUNCTION expensive(int) RETURNS boolean LANGUAGE sql
AS 'SELECT pg_sleep(1); SELECT true' COST 1000000;
CREATE TABLE t(c) AS SELECT * FROM generate_series(1,3);
EXPLAIN ANALYZE
SELECT * FROM (SELECT * FROM t WHERE expensive(c)) t0 WHERE c = 2;
That doesn't even use a view, let alone a security view. While I like the
patch's current simplicity, we need to narrow its impact.
Thanks,
nm
I was referring to this paragraph:
?On the technical side, I am pretty doubtful that the approach of adding a
?nestlevel to FuncExpr and RelOptInfo is the right way to go. ?I believe we
?have existing code (to handle left joins) that prevents quals from being
?pushed down too far by fudging the set of relations that are supposedly needed
?to evaluate the qual. ?I suspect a similar approach would work here.It seems to me the later half of this paragraph is talking about the problem of
unexpected qualifier pushing-down over the security barrier; I'm trying to solve
the problem with the part.2 patch.
The scenario the part.1 patch tries to solve is order to launch qualifiers, not
unexpected pushing-down.Okay, you're probably correct that it wasn't referring to the topic at hand.
I'm still suspicious of the silent assumption about how quals can be assigned
to plan nodes, but I don't have any concrete ideas for avoiding that.
If a subquery is enough simple to pull up, pull_up_simple_subquery() pulls up
the subquery. Its simpleness is checked by is_simple_subquery().
At that timing, qualifiers of the subquery are also pulled-up, so upper level
query shall have qualifiers with mixed nest-level.
Then, every qualifiers shall be distributed to a particular scan plan on
the distribute_qual_to_rels(); Its current criteria to distribute them is
"as deep as possible" according to the references of qualifiers.
This criteria cause a problem as I tried to tackle on the part.2 patch,
so it tries to put security-barrier to prevent unexpected pushing-down.
After the distribution, a scan plan will have qualifiers come from different
nest-levels. The order_qual_clauses() reorders the qualifiers according
to its cost estimation, so it possibly launches qualifiers come from upper
level prior to deeper one.
In addition, implementation will become complex, if both of qualifiers pulled-up
from security barrier view and qualifiers pulled-up from regular views are mixed
within a single qualifier list.I only scanned the part 2 patch, but isn't the bookkeeping already happening for
its purposes? ?How much more complexity would we get to apply the same strategy
to the behavior of this patch?If conditional, what criteria we should have to reorder the quelifier?
The current patch checks the depth at first, then it checks cost if same deptn.
It is quite simple rule. I have no idea of the criteria to order the
mixed qualifier
come from security-barrier views and regular views.Let's see. Every qual list will have some depth d such that all quals having
depth >= d are security-relevant, and all others are not security-relevant.
(This does not hold for all means of identifying security-relevant quals, but
it does hold for the CREATE SECURITY VIEW/reloptions strategy proposed in your
part 2 patch.) Suppose you track whether each Query node represents a
security view, then only increment the qualifier depth for such Query nodes,
rather than all Query nodes. The tracked depth then becomes a security
partition depth. Keep the actual sorting algorithm the same. (Disclaimer: I
haven't been thinking about this nearly as long as you have, so I may be
missing something relatively obvious.)
It might be an idea to increment the depth only when we go across security
barrier view. In other words, all the qualifiers will have same depth unless
it does not come from inside of the security view.
As it stands, the patch badly damages the performance of this example:
CREATE FUNCTION expensive(int) RETURNS boolean LANGUAGE sql
AS 'SELECT pg_sleep(1); SELECT true' COST 1000000;
CREATE TABLE t(c) AS SELECT * FROM generate_series(1,3);
EXPLAIN ANALYZE
SELECT * FROM (SELECT * FROM t WHERE expensive(c)) t0 WHERE c = 2;That doesn't even use a view, let alone a security view. While I like the
patch's current simplicity, we need to narrow its impact.
If we apply above idea I explained, c=2 and expensive(c) will belong
to same depth,
then it shall be reordered according to cost estimation.
In the case when "(SELECT * FROM t WHERE expensive(c))" come from security
view, the performance damage is unavoidable, because DBA explicitly specified
its main purpose is security.
So, it might be a good idea to split out my two patches into three.
1. Add "SECURITY VIEW" support.
2. Fix leaky view part.1 - order of qualifiers
3. Fix leaky view part.2 - unexpected pushing down
How about your opinion?
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
On Sat, Jul 02, 2011 at 12:48:32PM +0200, Kohei KaiGai wrote:
Let's see. ?Every qual list will have some depth d such that all quals having
depth >= d are security-relevant, and all others are not security-relevant.
(This does not hold for all means of identifying security-relevant quals, but
it does hold for the CREATE SECURITY VIEW/reloptions strategy proposed in your
part 2 patch.) ?Suppose you track whether each Query node represents a
security view, then only increment the qualifier depth for such Query nodes,
rather than all Query nodes. ?The tracked depth then becomes a security
partition depth. ?Keep the actual sorting algorithm the same. ?(Disclaimer: I
haven't been thinking about this nearly as long as you have, so I may be
missing something relatively obvious.)It might be an idea to increment the depth only when we go across security
barrier view. In other words, all the qualifiers will have same depth unless
it does not come from inside of the security view.
Yes; that sounds suitable.
As it stands, the patch badly damages the performance of this example:
CREATE FUNCTION expensive(int) RETURNS boolean LANGUAGE sql
? ? ? ?AS 'SELECT pg_sleep(1); SELECT true' COST 1000000;
CREATE TABLE t(c) AS SELECT * FROM generate_series(1,3);
EXPLAIN ANALYZE
? ? ? ?SELECT * FROM (SELECT * FROM t WHERE expensive(c)) t0 WHERE c = 2;That doesn't even use a view, let alone a security view. ?While I like the
patch's current simplicity, we need to narrow its impact.If we apply above idea I explained, c=2 and expensive(c) will belong
to same depth,
then it shall be reordered according to cost estimation.
In the case when "(SELECT * FROM t WHERE expensive(c))" come from security
view, the performance damage is unavoidable, because DBA explicitly specified
its main purpose is security.So, it might be a good idea to split out my two patches into three.
1. Add "SECURITY VIEW" support.
2. Fix leaky view part.1 - order of qualifiers
3. Fix leaky view part.2 - unexpected pushing downHow about your opinion?
I'd say, for CommitFest purposes, keep SECURITY VIEW attached to one of the
other patches. It's not likely it would be committed without anything hooked up
to actually use it. Splitting it out into its own patch *file* and attaching
that and the part 1 patch to the same email would be fine, though.
BTW, regarding to the statement support for security barrier views,
the following syntax might be more consistent with existing ones:
CREATE VIEW view_name WITH ( param [=value]) AS query ... ;
rather than
CREATE SECURITY VIEW view_name AS query ...;
Any comments?
2011/7/2 Noah Misch <noah@2ndquadrant.com>:
On Sat, Jul 02, 2011 at 12:48:32PM +0200, Kohei KaiGai wrote:
Let's see. ?Every qual list will have some depth d such that all quals having
depth >= d are security-relevant, and all others are not security-relevant.
(This does not hold for all means of identifying security-relevant quals, but
it does hold for the CREATE SECURITY VIEW/reloptions strategy proposed in your
part 2 patch.) ?Suppose you track whether each Query node represents a
security view, then only increment the qualifier depth for such Query nodes,
rather than all Query nodes. ?The tracked depth then becomes a security
partition depth. ?Keep the actual sorting algorithm the same. ?(Disclaimer: I
haven't been thinking about this nearly as long as you have, so I may be
missing something relatively obvious.)It might be an idea to increment the depth only when we go across security
barrier view. In other words, all the qualifiers will have same depth unless
it does not come from inside of the security view.Yes; that sounds suitable.
As it stands, the patch badly damages the performance of this example:
CREATE FUNCTION expensive(int) RETURNS boolean LANGUAGE sql
? ? ? ?AS 'SELECT pg_sleep(1); SELECT true' COST 1000000;
CREATE TABLE t(c) AS SELECT * FROM generate_series(1,3);
EXPLAIN ANALYZE
? ? ? ?SELECT * FROM (SELECT * FROM t WHERE expensive(c)) t0 WHERE c = 2;That doesn't even use a view, let alone a security view. ?While I like the
patch's current simplicity, we need to narrow its impact.If we apply above idea I explained, c=2 and expensive(c) will belong
to same depth,
then it shall be reordered according to cost estimation.
In the case when "(SELECT * FROM t WHERE expensive(c))" come from security
view, the performance damage is unavoidable, because DBA explicitly specified
its main purpose is security.So, it might be a good idea to split out my two patches into three.
1. Add "SECURITY VIEW" support.
2. Fix leaky view part.1 - order of qualifiers
3. Fix leaky view part.2 - unexpected pushing downHow about your opinion?
I'd say, for CommitFest purposes, keep SECURITY VIEW attached to one of the
other patches. It's not likely it would be committed without anything hooked up
to actually use it. Splitting it out into its own patch *file* and attaching
that and the part 1 patch to the same email would be fine, though.
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
On Sat, Jul 2, 2011 at 1:54 PM, Kohei KaiGai <kaigai@kaigai.gr.jp> wrote:
BTW, regarding to the statement support for security barrier views,
the following syntax might be more consistent with existing ones:
CREATE VIEW view_name WITH ( param [=value]) AS query ... ;
rather than
CREATE SECURITY VIEW view_name AS query ...;Any comments?
I think I mildly prefer CREATE SECURITY VIEW to the parameter syntax
in this case, but I don't hate the other one.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
Robert Haas <robertmhaas@gmail.com> writes:
On Sat, Jul 2, 2011 at 1:54 PM, Kohei KaiGai <kaigai@kaigai.gr.jp> wrote:
BTW, regarding to the statement support for security barrier views,
the following syntax might be more consistent with existing ones:
CREATE VIEW view_name WITH ( param [=value]) AS query ... ;
rather than
CREATE SECURITY VIEW view_name AS query ...;Any comments?
I think I mildly prefer CREATE SECURITY VIEW to the parameter syntax
in this case, but I don't hate the other one.
The WITH idea seems a bit more future-proof; in particular it would
easily accommodate specifying a security type, if we decide we need
various levels of leak-proof-ness.
regards, tom lane
On Sat, Jul 2, 2011 at 3:46 PM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
Robert Haas <robertmhaas@gmail.com> writes:
On Sat, Jul 2, 2011 at 1:54 PM, Kohei KaiGai <kaigai@kaigai.gr.jp> wrote:
BTW, regarding to the statement support for security barrier views,
the following syntax might be more consistent with existing ones:
CREATE VIEW view_name WITH ( param [=value]) AS query ... ;
rather than
CREATE SECURITY VIEW view_name AS query ...;Any comments?
I think I mildly prefer CREATE SECURITY VIEW to the parameter syntax
in this case, but I don't hate the other one.The WITH idea seems a bit more future-proof; in particular it would
easily accommodate specifying a security type, if we decide we need
various levels of leak-proof-ness.
Or other kinds of view options. I'm not going to argue against that
too forcefully, since I've advocated introducing that sort of syntax
elsewhere. I think it's mostly that I thought this feature might be
significant enough to merit a syntax that would make it a little more
prominent, but perhaps not.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
The attached patches are revised version.
The part-0 provides 'security_barrier' option for view definition, and performs
as a common basis of part-1 and part-2 patches.
Syntax is extended as follows:
CREATE VIEW view_name [WITH (param [=value])] AS query;
We can also turn on/off this security_barrier setting by ALTER TABLE with
SET/RESET options.
The part-1 patch enforces the qualifiers originally located under the security
barrier view to be launched prior to ones supplied on upper level.
The differences from the previous version is this barrier become conditional,
not always. So, existing optimization will be applied without any changes
onto non-security-barrier views.
Example)
postgres=# CREATE FUNCTION f_leak(text,text) RETURNS bool
COST 0.0001 LANGUAGE 'plpgsql'
AS 'begin raise notice ''% => %'', $1, $2; return true; end';
CREATE FUNCTION
postgres=# CREATE TABLE credit_card (cname text, cnumber text, cexpired text);
INSERT INTO credit_card (cname, cnumber, cexpired)
CREATE TABLE
postgres=# INSERT INTO credit_card (cname, cnumber, cexpired)
VALUES ('alice', '1111-2222-3333-4444', '07/2014'),
('bob', '5555-6666-7777-8888', '11/2013'),
('eve', '1234-5678-9012-3456', '05/2015');
INSERT 0 3
postgres=# CREATE VIEW my_credit_card AS SELECT * FROM credit_card
WHERE cname = getpgusername();
CREATE VIEW
postgres=# CREATE VIEW my_credit_card_sec WITH (security_barrier) AS
SELECT * FROM credit_card WHERE cname = getpgusername();
CREATE VIEW
postgres=# GRANT SELECT ON my_credit_card TO public;
GRANT
postgres=# GRANT SELECT ON my_credit_card_sec TO public;
GRANT
postgres=# SET SESSION AUTHORIZATION alice;
SET
postgres=> SELECT * FROM my_credit_card WHERE f_leak(cnumber,cexpired);
NOTICE: 1111-2222-3333-4444 => 07/2014
NOTICE: 5555-6666-7777-8888 => 11/2013
NOTICE: 1234-5678-9012-3456 => 05/2015
cname | cnumber | cexpired
-------+---------------------+----------
alice | 1111-2222-3333-4444 | 07/2014
(1 row)
postgres=> SELECT * FROM my_credit_card_sec WHERE f_leak(cnumber,cexpired);
NOTICE: 1111-2222-3333-4444 => 07/2014
cname | cnumber | cexpired
-------+---------------------+----------
alice | 1111-2222-3333-4444 | 07/2014
(1 row)
postgres=> EXPLAIN SELECT * FROM my_credit_card WHERE f_leak(cnumber,cexpired);
QUERY PLAN
-----------------------------------------------------------------------------
Seq Scan on credit_card (cost=0.00..21.20 rows=1 width=96)
Filter: (f_leak(cnumber, cexpired) AND (cname = (getpgusername())::text))
(2 rows)
postgres=> EXPLAIN SELECT * FROM my_credit_card_sec WHERE
f_leak(cnumber,cexpired);
QUERY PLAN
-----------------------------------------------------------------------------
Seq Scan on credit_card (cost=0.00..21.20 rows=1 width=96)
Filter: ((cname = (getpgusername())::text) AND f_leak(cnumber, cexpired))
(2 rows)
Thanks,
2011/7/3 Robert Haas <robertmhaas@gmail.com>:
On Sat, Jul 2, 2011 at 3:46 PM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
Robert Haas <robertmhaas@gmail.com> writes:
On Sat, Jul 2, 2011 at 1:54 PM, Kohei KaiGai <kaigai@kaigai.gr.jp> wrote:
BTW, regarding to the statement support for security barrier views,
the following syntax might be more consistent with existing ones:
CREATE VIEW view_name WITH ( param [=value]) AS query ... ;
rather than
CREATE SECURITY VIEW view_name AS query ...;Any comments?
I think I mildly prefer CREATE SECURITY VIEW to the parameter syntax
in this case, but I don't hate the other one.The WITH idea seems a bit more future-proof; in particular it would
easily accommodate specifying a security type, if we decide we need
various levels of leak-proof-ness.Or other kinds of view options. I'm not going to argue against that
too forcefully, since I've advocated introducing that sort of syntax
elsewhere. I think it's mostly that I thought this feature might be
significant enough to merit a syntax that would make it a little more
prominent, but perhaps not.--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
Attachments:
pgsql-v9.2-fix-leaky-view-part-0.v3.patchapplication/octet-stream; name=pgsql-v9.2-fix-leaky-view-part-0.v3.patchDownload
doc/src/sgml/ref/create_view.sgml | 24 ++++++++++++++++++++
src/backend/access/common/reloptions.c | 15 ++++++++++++-
src/backend/commands/tablecmds.c | 3 +-
src/backend/commands/view.c | 29 +++++++++++++++++++-----
src/backend/nodes/copyfuncs.c | 1 +
src/backend/nodes/equalfuncs.c | 1 +
src/backend/nodes/outfuncs.c | 1 +
src/backend/nodes/readfuncs.c | 1 +
src/backend/parser/gram.y | 10 +++++---
src/backend/rewrite/rewriteHandler.c | 1 +
src/backend/utils/cache/relcache.c | 1 +
src/bin/pg_dump/pg_dump.c | 6 +++-
src/include/access/reloptions.h | 3 +-
src/include/nodes/parsenodes.h | 3 ++
src/include/utils/rel.h | 11 ++++++++-
src/test/regress/expected/create_view.out | 34 ++++++++++++++++++++++++++++-
src/test/regress/sql/create_view.sql | 19 ++++++++++++++++
17 files changed, 146 insertions(+), 17 deletions(-)
diff --git a/doc/src/sgml/ref/create_view.sgml b/doc/src/sgml/ref/create_view.sgml
index 417f8c3..504ba27 100644
--- a/doc/src/sgml/ref/create_view.sgml
+++ b/doc/src/sgml/ref/create_view.sgml
@@ -22,6 +22,7 @@ PostgreSQL documentation
<refsynopsisdiv>
<synopsis>
CREATE [ OR REPLACE ] [ TEMP | TEMPORARY ] VIEW <replaceable class="PARAMETER">name</replaceable> [ ( <replaceable class="PARAMETER">column_name</replaceable> [, ...] ) ]
+ [ WITH ( parameter [= value] [, ... ] ) ]
AS <replaceable class="PARAMETER">query</replaceable>
</synopsis>
</refsynopsisdiv>
@@ -99,6 +100,29 @@ CREATE [ OR REPLACE ] [ TEMP | TEMPORARY ] VIEW <replaceable class="PARAMETER">n
</varlistentry>
<varlistentry>
+ <term><literal>WITH (<replaceable class="parameter">parameter [= value]</replaceable>)</literal></term>
+ <listitem>
+ <para>
+ This clause allows to specify optional parameters for a view.
+ </para>
+ <para>
+ If <literal>security_barrier=TRUE</literal> is specified, this view
+ shall performs as security barrier that prevent unexpected information
+ leaks. It is a recommendable configuration when the view is defined
+ to apply row-level security, in spite of performance trade-off.
+ </para>
+ <para>
+ It is a commonly-used technique that using views to filter out
+ tuple to be invisible to particular users, however, please note
+ that here is a known-problem that allows malicious users to
+ reference invisible tuples using a function with side-effect
+ because of interaction with query optimization.
+ See <xref linkend="rules-privileges"> for more detailed scenario.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
<term><replaceable class="parameter">query</replaceable></term>
<listitem>
<para>
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 4657425..57f3b46 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -66,6 +66,14 @@ static relopt_bool boolRelOpts[] =
},
true
},
+ {
+ {
+ "security_barrier",
+ "Prevent information leaks using functions with side-effects",
+ RELOPT_KIND_VIEW
+ },
+ false
+ },
/* list terminator */
{{NULL}}
};
@@ -776,6 +784,7 @@ extractRelOptions(HeapTuple tuple, TupleDesc tupdesc, Oid amoptions)
{
case RELKIND_RELATION:
case RELKIND_TOASTVALUE:
+ case RELKIND_VIEW:
case RELKIND_UNCATALOGED:
options = heap_reloptions(classForm->relkind, datum, false);
break;
@@ -1134,7 +1143,9 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
{"autovacuum_vacuum_scale_factor", RELOPT_TYPE_REAL,
offsetof(StdRdOptions, autovacuum) +offsetof(AutoVacOpts, vacuum_scale_factor)},
{"autovacuum_analyze_scale_factor", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) +offsetof(AutoVacOpts, analyze_scale_factor)}
+ offsetof(StdRdOptions, autovacuum) +offsetof(AutoVacOpts, analyze_scale_factor)},
+ {"security_barrier", RELOPT_TYPE_BOOL,
+ offsetof(StdRdOptions, security_barrier)},
};
options = parseRelOptions(reloptions, validate, kind, &numoptions);
@@ -1176,6 +1187,8 @@ heap_reloptions(char relkind, Datum reloptions, bool validate)
return (bytea *) rdopts;
case RELKIND_RELATION:
return default_reloptions(reloptions, validate, RELOPT_KIND_HEAP);
+ case RELKIND_VIEW:
+ return default_reloptions(reloptions, validate, RELOPT_KIND_VIEW);
default:
/* other relkinds are not supported */
return NULL;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index cfc685b..8e67330 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -2926,7 +2926,7 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
break;
case AT_SetRelOptions: /* SET (...) */
case AT_ResetRelOptions: /* RESET (...) */
- ATSimplePermissions(rel, ATT_TABLE | ATT_INDEX);
+ ATSimplePermissions(rel, ATT_TABLE | ATT_INDEX | ATT_VIEW);
/* This command never recurses */
/* No command-specific prep needed */
pass = AT_PASS_MISC;
@@ -7881,6 +7881,7 @@ ATExecSetRelOptions(Relation rel, List *defList, bool isReset, LOCKMODE lockmode
{
case RELKIND_RELATION:
case RELKIND_TOASTVALUE:
+ case RELKIND_VIEW:
(void) heap_reloptions(rel->rd_rel->relkind, newOptions, true);
break;
case RELKIND_INDEX:
diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c
index be681e3..92102e7 100644
--- a/src/backend/commands/view.c
+++ b/src/backend/commands/view.c
@@ -97,7 +97,8 @@ isViewOnTempTable_walker(Node *node, void *context)
*---------------------------------------------------------------------
*/
static Oid
-DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace)
+DefineVirtualRelation(const RangeVar *relation, List *tlist,
+ bool replace, List *options)
{
Oid viewOid,
namespaceId;
@@ -167,6 +168,8 @@ DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace)
{
Relation rel;
TupleDesc descriptor;
+ List *atcmds = NIL;
+ AlterTableCmd *atcmd;
/*
* Yes. Get exclusive lock on the existing view ...
@@ -211,14 +214,11 @@ DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace)
*/
if (list_length(attrList) > rel->rd_att->natts)
{
- List *atcmds = NIL;
ListCell *c;
int skip = rel->rd_att->natts;
foreach(c, attrList)
{
- AlterTableCmd *atcmd;
-
if (skip > 0)
{
skip--;
@@ -229,10 +229,24 @@ DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace)
atcmd->def = (Node *) lfirst(c);
atcmds = lappend(atcmds, atcmd);
}
- AlterTableInternal(viewOid, atcmds, true);
}
/*
+ * If optional parameters are specified, we must set options
+ * using ALTER TABLE SET OPTION internally.
+ */
+ if (list_length(options) > 0)
+ {
+ atcmd = makeNode(AlterTableCmd);
+ atcmd->subtype = AT_SetRelOptions;
+ atcmd->def = options;
+
+ atcmds = lappend(atcmds, atcmd);
+ }
+ if (atcmds != NIL)
+ AlterTableInternal(viewOid, atcmds, true);
+
+ /*
* Seems okay, so return the OID of the pre-existing view.
*/
relation_close(rel, NoLock); /* keep the lock! */
@@ -256,6 +270,9 @@ DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace)
createStmt->tablespacename = NULL;
createStmt->if_not_exists = false;
+ if (options != NIL)
+ createStmt->options = list_concat(createStmt->options, options);
+
/*
* finally create the relation (this will error out if there's an
* existing view, so we don't need more code to complain if "replace"
@@ -510,7 +527,7 @@ DefineView(ViewStmt *stmt, const char *queryString)
* aborted.
*/
viewOid = DefineVirtualRelation(view, viewParse->targetList,
- stmt->replace);
+ stmt->replace, stmt->options);
/*
* The relation we have just created is not visible to any other commands
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index c9133dd..4fb60a1 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -1944,6 +1944,7 @@ _copyRangeTblEntry(RangeTblEntry *from)
COPY_SCALAR_FIELD(relid);
COPY_SCALAR_FIELD(relkind);
COPY_NODE_FIELD(subquery);
+ COPY_SCALAR_FIELD(security_barrier);
COPY_SCALAR_FIELD(jointype);
COPY_NODE_FIELD(joinaliasvars);
COPY_NODE_FIELD(funcexpr);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 3a0267c..d201f22 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -2303,6 +2303,7 @@ _equalRangeTblEntry(RangeTblEntry *a, RangeTblEntry *b)
COMPARE_SCALAR_FIELD(relid);
COMPARE_SCALAR_FIELD(relkind);
COMPARE_NODE_FIELD(subquery);
+ COMPARE_SCALAR_FIELD(security_barrier);
COMPARE_SCALAR_FIELD(jointype);
COMPARE_NODE_FIELD(joinaliasvars);
COMPARE_NODE_FIELD(funcexpr);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 681f5f8..04a8760 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -2311,6 +2311,7 @@ _outRangeTblEntry(StringInfo str, RangeTblEntry *node)
break;
case RTE_SUBQUERY:
WRITE_NODE_FIELD(subquery);
+ WRITE_BOOL_FIELD(security_barrier);
break;
case RTE_JOIN:
WRITE_ENUM_FIELD(jointype, JoinType);
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 29a0e8f..098f3c3 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -1191,6 +1191,7 @@ _readRangeTblEntry(void)
break;
case RTE_SUBQUERY:
READ_NODE_FIELD(subquery);
+ READ_BOOL_FIELD(security_barrier);
break;
case RTE_JOIN:
READ_ENUM_FIELD(jointype, JoinType);
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 7226032..517cdf3 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -7266,26 +7266,28 @@ transaction_mode_list_or_empty:
*
*****************************************************************************/
-ViewStmt: CREATE OptTemp VIEW qualified_name opt_column_list
+ViewStmt: CREATE OptTemp VIEW qualified_name opt_column_list opt_reloptions
AS SelectStmt opt_check_option
{
ViewStmt *n = makeNode(ViewStmt);
n->view = $4;
n->view->relpersistence = $2;
n->aliases = $5;
- n->query = $7;
+ n->query = $8;
n->replace = false;
+ n->options = $6;
$$ = (Node *) n;
}
- | CREATE OR REPLACE OptTemp VIEW qualified_name opt_column_list
+ | CREATE OR REPLACE OptTemp VIEW qualified_name opt_column_list opt_reloptions
AS SelectStmt opt_check_option
{
ViewStmt *n = makeNode(ViewStmt);
n->view = $6;
n->view->relpersistence = $4;
n->aliases = $7;
- n->query = $9;
+ n->query = $10;
n->replace = true;
+ n->options = $8;
$$ = (Node *) n;
}
;
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index 6ef20a5..affe103 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -1382,6 +1382,7 @@ ApplyRetrieveRule(Query *parsetree,
rte->rtekind = RTE_SUBQUERY;
rte->relid = InvalidOid;
rte->subquery = rule_action;
+ rte->security_barrier = RelationIsSecurityView(relation);
rte->inh = false; /* must not be set for a subquery */
/*
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 0b9d77a..4eb1ee8 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -377,6 +377,7 @@ RelationParseRelOptions(Relation relation, HeapTuple tuple)
case RELKIND_RELATION:
case RELKIND_TOASTVALUE:
case RELKIND_INDEX:
+ case RELKIND_VIEW:
break;
default:
return;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 9e69b0f..ab2b0cb 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -12023,8 +12023,10 @@ dumpTableSchema(Archive *fout, TableInfo *tbinfo)
if (binary_upgrade)
binary_upgrade_set_pg_class_oids(q, tbinfo->dobj.catId.oid, false);
- appendPQExpBuffer(q, "CREATE VIEW %s AS\n %s\n",
- fmtId(tbinfo->dobj.name), viewdef);
+ appendPQExpBuffer(q, "CREATE VIEW %s", fmtId(tbinfo->dobj.name));
+ if (tbinfo->reloptions && strlen(tbinfo->reloptions) > 0)
+ appendPQExpBuffer(q, " WITH (%s)", tbinfo->reloptions);
+ appendPQExpBuffer(q, " AS\n %s\n", viewdef);
appendPQExpBuffer(labelq, "VIEW %s",
fmtId(tbinfo->dobj.name));
diff --git a/src/include/access/reloptions.h b/src/include/access/reloptions.h
index c7709cc..586236e 100644
--- a/src/include/access/reloptions.h
+++ b/src/include/access/reloptions.h
@@ -42,8 +42,9 @@ typedef enum relopt_kind
RELOPT_KIND_GIST = (1 << 5),
RELOPT_KIND_ATTRIBUTE = (1 << 6),
RELOPT_KIND_TABLESPACE = (1 << 7),
+ RELOPT_KIND_VIEW = (1 << 8),
/* if you add a new kind, make sure you update "last_default" too */
- RELOPT_KIND_LAST_DEFAULT = RELOPT_KIND_TABLESPACE,
+ RELOPT_KIND_LAST_DEFAULT = RELOPT_KIND_VIEW,
/* some compilers treat enums as signed ints, so we can't use 1 << 31 */
RELOPT_KIND_MAX = (1 << 30)
} relopt_kind;
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 00c1269..4723c28 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -706,6 +706,8 @@ typedef struct RangeTblEntry
*/
Query *subquery; /* the sub-query */
+ bool security_barrier; /* Was a security barrier view? */
+
/*
* Fields valid for a join RTE (else NULL/zero):
*
@@ -2339,6 +2341,7 @@ typedef struct ViewStmt
List *aliases; /* target column names */
Node *query; /* the SELECT query */
bool replace; /* replace an existing view? */
+ List *options; /* options from WITH clause */
} ViewStmt;
/* ----------------------
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index e2c2fa9..e1272af 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -221,7 +221,7 @@ typedef struct RelationData
/*
* StdRdOptions
- * Standard contents of rd_options for heaps and generic indexes.
+ * Standard contents of rd_options for heaps, views and generic indexes.
*
* RelationGetFillFactor() and RelationGetTargetPageFreeSpace() can only
* be applied to relations that use this format or a superset for
@@ -247,6 +247,7 @@ typedef struct StdRdOptions
int32 vl_len_; /* varlena header (do not touch directly!) */
int fillfactor; /* page fill factor in percent (0..100) */
AutoVacOpts autovacuum; /* autovacuum-related options */
+ bool security_barrier; /* performs as security-barrier view */
} StdRdOptions;
#define HEAP_MIN_FILLFACTOR 10
@@ -275,6 +276,14 @@ typedef struct StdRdOptions
(BLCKSZ * (100 - RelationGetFillFactor(relation, defaultff)) / 100)
/*
+ * RelationIsSecurityView
+ * Returns whether the relation is security view, or not
+ */
+#define RelationIsSecurityView(relation) \
+ ((relation)->rd_options ? \
+ ((StdRdOptions *) (relation)->rd_options)->security_barrier : false)
+
+/*
* RelationIsValid
* True iff relation descriptor is valid.
*/
diff --git a/src/test/regress/expected/create_view.out b/src/test/regress/expected/create_view.out
index f2c0685..7cc3000 100644
--- a/src/test/regress/expected/create_view.out
+++ b/src/test/regress/expected/create_view.out
@@ -239,6 +239,34 @@ And relnamespace IN (SELECT OID FROM pg_namespace WHERE nspname LIKE 'pg_temp%')
1
(1 row)
+--Should work correctly to leaky-view scenario
+CREATE TABLE lvtest1 (a int, b text);
+CREATE TABLE lvtest2 (x int, y text);
+INSERT INTO lvtest1 VALUES (10, 'aaa'), (11, 'bbb'), (12, 'ccc'), (13, 'ddd');
+INSERT INTO lvtest2 VALUES (11, 'xxx'), (12, 'yyy'), (13, 'zzz'), (14, 'xyz');
+CREATE OR REPLACE VIEW leaky_v1 WITH (security_barrier=true) AS
+ SELECT * FROM lvtest1 JOIN lvtest2 ON a = x WHERE a % 2 = 0; -- OK
+CREATE OR REPLACE VIEW leaky_v2 WITH (security_barrier=false) AS
+ SELECT * FROM lvtest1 JOIN lvtest2 ON a = x WHERE a % 2 = 1; -- OK
+CREATE OR REPLACE VIEW leaky_v3 WITH (invalid_option=1234) AS
+ SELECT * FROM lvtest1 JOIN lvtest2 ON a = x WHERE a % 2 = 1; -- Fail
+ERROR: unrecognized parameter "invalid_option"
+SELECT relname, reloptions FROM pg_class WHERE relname like 'leaky_v%';
+ relname | reloptions
+----------+--------------------------
+ leaky_v1 | {security_barrier=true}
+ leaky_v2 | {security_barrier=false}
+(2 rows)
+
+ALTER TABLE leaky_v1 RESET ( security_barrier );
+ALTER TABLE leaky_v2 SET ( security_barrier );
+SELECT relname, reloptions FROM pg_class WHERE relname like 'leaky_v%';
+ relname | reloptions
+----------+-------------------------
+ leaky_v1 |
+ leaky_v2 | {security_barrier=true}
+(2 rows)
+
DROP SCHEMA temp_view_test CASCADE;
NOTICE: drop cascades to 22 other objects
DETAIL: drop cascades to table temp_view_test.base_table
@@ -264,7 +292,7 @@ drop cascades to view temp_view_test.v8
drop cascades to sequence temp_view_test.seq1
drop cascades to view temp_view_test.v9
DROP SCHEMA testviewschm2 CASCADE;
-NOTICE: drop cascades to 16 other objects
+NOTICE: drop cascades to 20 other objects
DETAIL: drop cascades to table t1
drop cascades to view temporal1
drop cascades to view temporal2
@@ -281,4 +309,8 @@ drop cascades to table tbl3
drop cascades to table tbl4
drop cascades to view mytempview
drop cascades to view pubview
+drop cascades to table lvtest1
+drop cascades to table lvtest2
+drop cascades to view leaky_v1
+drop cascades to view leaky_v2
SET search_path to public;
diff --git a/src/test/regress/sql/create_view.sql b/src/test/regress/sql/create_view.sql
index 86cfc51..fbcd25f 100644
--- a/src/test/regress/sql/create_view.sql
+++ b/src/test/regress/sql/create_view.sql
@@ -191,6 +191,25 @@ AND NOT EXISTS (SELECT g FROM tbl4 LEFT JOIN tmptbl ON tbl4.h = tmptbl.j);
SELECT count(*) FROM pg_class where relname LIKE 'mytempview'
And relnamespace IN (SELECT OID FROM pg_namespace WHERE nspname LIKE 'pg_temp%');
+--Should work correctly to leaky-view scenario
+CREATE TABLE lvtest1 (a int, b text);
+CREATE TABLE lvtest2 (x int, y text);
+
+INSERT INTO lvtest1 VALUES (10, 'aaa'), (11, 'bbb'), (12, 'ccc'), (13, 'ddd');
+INSERT INTO lvtest2 VALUES (11, 'xxx'), (12, 'yyy'), (13, 'zzz'), (14, 'xyz');
+
+CREATE OR REPLACE VIEW leaky_v1 WITH (security_barrier=true) AS
+ SELECT * FROM lvtest1 JOIN lvtest2 ON a = x WHERE a % 2 = 0; -- OK
+CREATE OR REPLACE VIEW leaky_v2 WITH (security_barrier=false) AS
+ SELECT * FROM lvtest1 JOIN lvtest2 ON a = x WHERE a % 2 = 1; -- OK
+CREATE OR REPLACE VIEW leaky_v3 WITH (invalid_option=1234) AS
+ SELECT * FROM lvtest1 JOIN lvtest2 ON a = x WHERE a % 2 = 1; -- Fail
+SELECT relname, reloptions FROM pg_class WHERE relname like 'leaky_v%';
+
+ALTER TABLE leaky_v1 RESET ( security_barrier );
+ALTER TABLE leaky_v2 SET ( security_barrier );
+SELECT relname, reloptions FROM pg_class WHERE relname like 'leaky_v%';
+
DROP SCHEMA temp_view_test CASCADE;
DROP SCHEMA testviewschm2 CASCADE;
pgsql-v9.2-fix-leaky-view-part-1.v3.patchapplication/octet-stream; name=pgsql-v9.2-fix-leaky-view-part-1.v3.patchDownload
src/backend/nodes/copyfuncs.c | 8 ++
src/backend/nodes/equalfuncs.c | 8 ++
src/backend/nodes/outfuncs.c | 8 ++
src/backend/nodes/readfuncs.c | 8 ++
src/backend/optimizer/path/costsize.c | 17 ++++
src/backend/optimizer/plan/createplan.c | 13 +++-
src/backend/optimizer/plan/planner.c | 133 ++++++++++++++++++++++++++++
src/backend/optimizer/util/clauses.c | 24 ++++--
src/include/nodes/primnodes.h | 6 ++
src/include/nodes/relation.h | 1 +
src/test/regress/expected/select_views.out | 91 ++++++++++++++++---
src/test/regress/sql/select_views.sql | 39 ++++++++
12 files changed, 333 insertions(+), 23 deletions(-)
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 4fb60a1..b2fbfec 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -1188,6 +1188,7 @@ _copyFuncExpr(FuncExpr *from)
COPY_SCALAR_FIELD(inputcollid);
COPY_NODE_FIELD(args);
COPY_LOCATION_FIELD(location);
+ COPY_SCALAR_FIELD(depth);
return newnode;
}
@@ -1224,6 +1225,7 @@ _copyOpExpr(OpExpr *from)
COPY_SCALAR_FIELD(inputcollid);
COPY_NODE_FIELD(args);
COPY_LOCATION_FIELD(location);
+ COPY_SCALAR_FIELD(depth);
return newnode;
}
@@ -1244,6 +1246,7 @@ _copyDistinctExpr(DistinctExpr *from)
COPY_SCALAR_FIELD(inputcollid);
COPY_NODE_FIELD(args);
COPY_LOCATION_FIELD(location);
+ COPY_SCALAR_FIELD(depth);
return newnode;
}
@@ -1264,6 +1267,7 @@ _copyNullIfExpr(NullIfExpr *from)
COPY_SCALAR_FIELD(inputcollid);
COPY_NODE_FIELD(args);
COPY_LOCATION_FIELD(location);
+ COPY_SCALAR_FIELD(depth);
return newnode;
}
@@ -1282,6 +1286,7 @@ _copyScalarArrayOpExpr(ScalarArrayOpExpr *from)
COPY_SCALAR_FIELD(inputcollid);
COPY_NODE_FIELD(args);
COPY_LOCATION_FIELD(location);
+ COPY_SCALAR_FIELD(depth);
return newnode;
}
@@ -1422,6 +1427,7 @@ _copyCoerceViaIO(CoerceViaIO *from)
COPY_SCALAR_FIELD(resultcollid);
COPY_SCALAR_FIELD(coerceformat);
COPY_LOCATION_FIELD(location);
+ COPY_SCALAR_FIELD(depth);
return newnode;
}
@@ -1442,6 +1448,7 @@ _copyArrayCoerceExpr(ArrayCoerceExpr *from)
COPY_SCALAR_FIELD(isExplicit);
COPY_SCALAR_FIELD(coerceformat);
COPY_LOCATION_FIELD(location);
+ COPY_SCALAR_FIELD(depth);
return newnode;
}
@@ -1574,6 +1581,7 @@ _copyRowCompareExpr(RowCompareExpr *from)
COPY_NODE_FIELD(inputcollids);
COPY_NODE_FIELD(largs);
COPY_NODE_FIELD(rargs);
+ COPY_SCALAR_FIELD(depth);
return newnode;
}
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index d201f22..6d0839d 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -248,6 +248,7 @@ _equalFuncExpr(FuncExpr *a, FuncExpr *b)
COMPARE_SCALAR_FIELD(inputcollid);
COMPARE_NODE_FIELD(args);
COMPARE_LOCATION_FIELD(location);
+ COMPARE_SCALAR_FIELD(depth);
return true;
}
@@ -285,6 +286,7 @@ _equalOpExpr(OpExpr *a, OpExpr *b)
COMPARE_SCALAR_FIELD(inputcollid);
COMPARE_NODE_FIELD(args);
COMPARE_LOCATION_FIELD(location);
+ COMPARE_SCALAR_FIELD(depth);
return true;
}
@@ -311,6 +313,7 @@ _equalDistinctExpr(DistinctExpr *a, DistinctExpr *b)
COMPARE_SCALAR_FIELD(inputcollid);
COMPARE_NODE_FIELD(args);
COMPARE_LOCATION_FIELD(location);
+ COMPARE_SCALAR_FIELD(depth);
return true;
}
@@ -337,6 +340,7 @@ _equalNullIfExpr(NullIfExpr *a, NullIfExpr *b)
COMPARE_SCALAR_FIELD(inputcollid);
COMPARE_NODE_FIELD(args);
COMPARE_LOCATION_FIELD(location);
+ COMPARE_SCALAR_FIELD(depth);
return true;
}
@@ -361,6 +365,7 @@ _equalScalarArrayOpExpr(ScalarArrayOpExpr *a, ScalarArrayOpExpr *b)
COMPARE_SCALAR_FIELD(inputcollid);
COMPARE_NODE_FIELD(args);
COMPARE_LOCATION_FIELD(location);
+ COMPARE_SCALAR_FIELD(depth);
return true;
}
@@ -479,6 +484,7 @@ _equalCoerceViaIO(CoerceViaIO *a, CoerceViaIO *b)
return false;
COMPARE_LOCATION_FIELD(location);
+ COMPARE_SCALAR_FIELD(depth);
return true;
}
@@ -503,6 +509,7 @@ _equalArrayCoerceExpr(ArrayCoerceExpr *a, ArrayCoerceExpr *b)
return false;
COMPARE_LOCATION_FIELD(location);
+ COMPARE_SCALAR_FIELD(depth);
return true;
}
@@ -613,6 +620,7 @@ _equalRowCompareExpr(RowCompareExpr *a, RowCompareExpr *b)
COMPARE_NODE_FIELD(inputcollids);
COMPARE_NODE_FIELD(largs);
COMPARE_NODE_FIELD(rargs);
+ COMPARE_SCALAR_FIELD(depth);
return true;
}
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 04a8760..0b499fb 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -1004,6 +1004,7 @@ _outFuncExpr(StringInfo str, FuncExpr *node)
WRITE_OID_FIELD(inputcollid);
WRITE_NODE_FIELD(args);
WRITE_LOCATION_FIELD(location);
+ WRITE_INT_FIELD(depth);
}
static void
@@ -1030,6 +1031,7 @@ _outOpExpr(StringInfo str, OpExpr *node)
WRITE_OID_FIELD(inputcollid);
WRITE_NODE_FIELD(args);
WRITE_LOCATION_FIELD(location);
+ WRITE_INT_FIELD(depth);
}
static void
@@ -1045,6 +1047,7 @@ _outDistinctExpr(StringInfo str, DistinctExpr *node)
WRITE_OID_FIELD(inputcollid);
WRITE_NODE_FIELD(args);
WRITE_LOCATION_FIELD(location);
+ WRITE_INT_FIELD(depth);
}
static void
@@ -1060,6 +1063,7 @@ _outNullIfExpr(StringInfo str, NullIfExpr *node)
WRITE_OID_FIELD(inputcollid);
WRITE_NODE_FIELD(args);
WRITE_LOCATION_FIELD(location);
+ WRITE_INT_FIELD(depth);
}
static void
@@ -1073,6 +1077,7 @@ _outScalarArrayOpExpr(StringInfo str, ScalarArrayOpExpr *node)
WRITE_OID_FIELD(inputcollid);
WRITE_NODE_FIELD(args);
WRITE_LOCATION_FIELD(location);
+ WRITE_INT_FIELD(depth);
}
static void
@@ -1190,6 +1195,7 @@ _outCoerceViaIO(StringInfo str, CoerceViaIO *node)
WRITE_OID_FIELD(resultcollid);
WRITE_ENUM_FIELD(coerceformat, CoercionForm);
WRITE_LOCATION_FIELD(location);
+ WRITE_INT_FIELD(depth);
}
static void
@@ -1205,6 +1211,7 @@ _outArrayCoerceExpr(StringInfo str, ArrayCoerceExpr *node)
WRITE_BOOL_FIELD(isExplicit);
WRITE_ENUM_FIELD(coerceformat, CoercionForm);
WRITE_LOCATION_FIELD(location);
+ WRITE_INT_FIELD(depth);
}
static void
@@ -1297,6 +1304,7 @@ _outRowCompareExpr(StringInfo str, RowCompareExpr *node)
WRITE_NODE_FIELD(inputcollids);
WRITE_NODE_FIELD(largs);
WRITE_NODE_FIELD(rargs);
+ WRITE_INT_FIELD(depth);
}
static void
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 098f3c3..21ec851 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -541,6 +541,7 @@ _readFuncExpr(void)
READ_OID_FIELD(inputcollid);
READ_NODE_FIELD(args);
READ_LOCATION_FIELD(location);
+ READ_INT_FIELD(depth);
READ_DONE();
}
@@ -588,6 +589,7 @@ _readOpExpr(void)
READ_OID_FIELD(inputcollid);
READ_NODE_FIELD(args);
READ_LOCATION_FIELD(location);
+ READ_INT_FIELD(depth);
READ_DONE();
}
@@ -619,6 +621,7 @@ _readDistinctExpr(void)
READ_OID_FIELD(inputcollid);
READ_NODE_FIELD(args);
READ_LOCATION_FIELD(location);
+ READ_INT_FIELD(depth);
READ_DONE();
}
@@ -650,6 +653,7 @@ _readNullIfExpr(void)
READ_OID_FIELD(inputcollid);
READ_NODE_FIELD(args);
READ_LOCATION_FIELD(location);
+ READ_INT_FIELD(depth);
READ_DONE();
}
@@ -679,6 +683,7 @@ _readScalarArrayOpExpr(void)
READ_OID_FIELD(inputcollid);
READ_NODE_FIELD(args);
READ_LOCATION_FIELD(location);
+ READ_INT_FIELD(depth);
READ_DONE();
}
@@ -794,6 +799,7 @@ _readCoerceViaIO(void)
READ_OID_FIELD(resultcollid);
READ_ENUM_FIELD(coerceformat, CoercionForm);
READ_LOCATION_FIELD(location);
+ READ_INT_FIELD(depth);
READ_DONE();
}
@@ -814,6 +820,7 @@ _readArrayCoerceExpr(void)
READ_BOOL_FIELD(isExplicit);
READ_ENUM_FIELD(coerceformat, CoercionForm);
READ_LOCATION_FIELD(location);
+ READ_INT_FIELD(depth);
READ_DONE();
}
@@ -946,6 +953,7 @@ _readRowCompareExpr(void)
READ_NODE_FIELD(inputcollids);
READ_NODE_FIELD(largs);
READ_NODE_FIELD(rargs);
+ READ_INT_FIELD(depth);
READ_DONE();
}
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index bb38768..0302ad2 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -2593,6 +2593,7 @@ cost_qual_eval(QualCost *cost, List *quals, PlannerInfo *root)
context.root = root;
context.total.startup = 0;
context.total.per_tuple = 0;
+ context.total.depth = 0;
/* We don't charge any cost for the implicit ANDing at top level ... */
@@ -2618,6 +2619,7 @@ cost_qual_eval_node(QualCost *cost, Node *qual, PlannerInfo *root)
context.root = root;
context.total.startup = 0;
context.total.per_tuple = 0;
+ context.total.depth = 0;
cost_qual_eval_walker(qual, &context);
@@ -2647,6 +2649,7 @@ cost_qual_eval_walker(Node *node, cost_qual_eval_context *context)
locContext.root = context->root;
locContext.total.startup = 0;
locContext.total.per_tuple = 0;
+ locContext.total.depth = 0;
/*
* For an OR clause, recurse into the marked-up tree so that we
@@ -2671,6 +2674,8 @@ cost_qual_eval_walker(Node *node, cost_qual_eval_context *context)
}
context->total.startup += rinfo->eval_cost.startup;
context->total.per_tuple += rinfo->eval_cost.per_tuple;
+ if (rinfo->eval_cost.depth > context->total.depth)
+ context->total.depth = rinfo->eval_cost.depth;
/* do NOT recurse into children */
return false;
}
@@ -2694,6 +2699,8 @@ cost_qual_eval_walker(Node *node, cost_qual_eval_context *context)
{
context->total.per_tuple +=
get_func_cost(((FuncExpr *) node)->funcid) * cpu_operator_cost;
+ if (((FuncExpr *)node)->depth > context->total.depth)
+ context->total.depth = ((FuncExpr *)node)->depth;
}
else if (IsA(node, OpExpr) ||
IsA(node, DistinctExpr) ||
@@ -2703,6 +2710,8 @@ cost_qual_eval_walker(Node *node, cost_qual_eval_context *context)
set_opfuncid((OpExpr *) node);
context->total.per_tuple +=
get_func_cost(((OpExpr *) node)->opfuncid) * cpu_operator_cost;
+ if (((OpExpr *)node)->depth > context->total.depth)
+ context->total.depth = ((OpExpr *)node)->depth;
}
else if (IsA(node, ScalarArrayOpExpr))
{
@@ -2716,6 +2725,8 @@ cost_qual_eval_walker(Node *node, cost_qual_eval_context *context)
set_sa_opfuncid(saop);
context->total.per_tuple += get_func_cost(saop->opfuncid) *
cpu_operator_cost * estimate_array_length(arraynode) * 0.5;
+ if (saop->depth > context->total.depth)
+ context->total.depth = saop->depth;
}
else if (IsA(node, Aggref) ||
IsA(node, WindowFunc))
@@ -2746,6 +2757,8 @@ cost_qual_eval_walker(Node *node, cost_qual_eval_context *context)
getTypeOutputInfo(exprType((Node *) iocoerce->arg),
&iofunc, &typisvarlena);
context->total.per_tuple += get_func_cost(iofunc) * cpu_operator_cost;
+ if (iocoerce->depth > context->total.depth)
+ context->total.depth = iocoerce->depth;
}
else if (IsA(node, ArrayCoerceExpr))
{
@@ -2755,6 +2768,8 @@ cost_qual_eval_walker(Node *node, cost_qual_eval_context *context)
if (OidIsValid(acoerce->elemfuncid))
context->total.per_tuple += get_func_cost(acoerce->elemfuncid) *
cpu_operator_cost * estimate_array_length(arraynode);
+ if (acoerce->depth > context->total.depth)
+ context->total.depth = acoerce->depth;
}
else if (IsA(node, RowCompareExpr))
{
@@ -2769,6 +2784,8 @@ cost_qual_eval_walker(Node *node, cost_qual_eval_context *context)
context->total.per_tuple += get_func_cost(get_opcode(opid)) *
cpu_operator_cost;
}
+ if (rcexpr->depth > context->total.depth)
+ context->total.depth = rcexpr->depth;
}
else if (IsA(node, CurrentOfExpr))
{
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index e4ccf5c..3414b4c 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -2704,6 +2704,7 @@ order_qual_clauses(PlannerInfo *root, List *clauses)
{
Node *clause;
Cost cost;
+ int depth;
} QualItem;
int nitems = list_length(clauses);
QualItem *items;
@@ -2729,6 +2730,7 @@ order_qual_clauses(PlannerInfo *root, List *clauses)
cost_qual_eval_node(&qcost, clause, root);
items[i].clause = clause;
items[i].cost = qcost.per_tuple;
+ items[i].depth = qcost.depth;
i++;
}
@@ -2745,7 +2747,16 @@ order_qual_clauses(PlannerInfo *root, List *clauses)
/* insert newitem into the already-sorted subarray */
for (j = i; j > 0; j--)
{
- if (newitem.cost >= items[j - 1].cost)
+ /*
+ * Higher priority shall be given to the items originated from
+ * deeper nest level. If same level, it shall be given to the
+ * items with smaller estimated cost.
+ * Such kind of consideration is needed to prevent leaky-view
+ * problem.
+ */
+ if (newitem.depth < items[j - 1].depth ||
+ (newitem.depth == items[j - 1].depth &&
+ newitem.cost >= items[j - 1].cost))
break;
items[j] = items[j - 1];
}
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 9aafc8a..b014051 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -33,6 +33,7 @@
#include "optimizer/subselect.h"
#include "optimizer/tlist.h"
#include "optimizer/var.h"
+#include "nodes/nodeFuncs.h"
#ifdef OPTIMIZER_DEBUG
#include "nodes/print.h"
#endif
@@ -103,6 +104,7 @@ static void get_column_info_for_window(PlannerInfo *root, WindowClause *wc,
int *ordNumCols,
AttrNumber **ordColIdx,
Oid **ordOperators);
+static void mark_qualifiers_depth(Query *query, int depth);
/*****************************************************************************
@@ -148,6 +150,12 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams)
cursorOptions |= ((DeclareCursorStmt *) parse->utilityStmt)->options;
/*
+ * Mark qualifiers its original depth to prevent reversal of orders
+ * on evaluation of WHERE clause during relation scanns.
+ */
+ mark_qualifiers_depth(parse, 0);
+
+ /*
* Set up global state for this planner invocation. This data is needed
* across all levels of sub-Query that might exist in the given command,
* so we keep it in a separate struct that's linked to by each per-Query
@@ -2993,6 +3001,131 @@ get_column_info_for_window(PlannerInfo *root, WindowClause *wc, List *tlist,
}
}
+/*
+ * mark_qualifiers_depth
+ *
+ * It marks depth field of the each expression nodes that eventually
+ * invokes functions, to track the original nest-level. On the evaluation
+ * of qualifiers within WHERE or JOIN ... ON clauses during relation scans,
+ * these items shall be reordered according to the nest-level and estimated
+ * cost.
+ * The optimizer may pull-up simple sub-queries or join clause, and
+ * qualifiers to filter out tuples shall be mixed with ones in upper-
+ * level. Thus, we need to track the original nest-level of qualifiers
+ * to prevent reverse of order in evaluation, because some of qualifiers
+ * can have side-effects that allows to leak supplied argument to outside.
+ * It can be abused to break row-level security using a user defined function
+ * with very small estimated cost, so nest level of qualifiers originated
+ * from is used as a criteria, rather than estimated cost, to decide order
+ * to evaluate qualifiers.
+ */
+static bool
+mark_qualifiers_depth_walker(Node *node, void *context)
+{
+ int depth = *((int *)(context));
+
+ if (node == NULL)
+ return false;
+ if (IsA(node, FuncExpr))
+ {
+ FuncExpr *exp = (FuncExpr *)node;
+
+ exp->depth = depth | (exp->depth & 1);
+
+ return false;
+ }
+ else if (IsA(node, OpExpr))
+ {
+ OpExpr *exp = (OpExpr *)node;
+
+ exp->depth = depth | (exp->depth & 1);
+
+ return false;
+ }
+ else if (IsA(node, DistinctExpr))
+ {
+ DistinctExpr *exp = (DistinctExpr *)node;
+
+ exp->depth = depth | (exp->depth & 1);
+
+ return false;
+ }
+ else if (IsA(node, ScalarArrayOpExpr))
+ {
+ ScalarArrayOpExpr *exp = (ScalarArrayOpExpr *)node;
+
+ exp->depth = depth | (exp->depth & 1);
+
+ return false;
+ }
+ else if (IsA(node, CoerceViaIO))
+ {
+ CoerceViaIO *exp = (CoerceViaIO *)node;
+
+ exp->depth = depth | (exp->depth & 1);
+
+ return false;
+ }
+ else if (IsA(node, ArrayCoerceExpr))
+ {
+ ArrayCoerceExpr *exp = (ArrayCoerceExpr *)node;
+
+ exp->depth = depth | (exp->depth & 1);
+
+ return false;
+ }
+ else if (IsA(node, NullIfExpr))
+ {
+ NullIfExpr *exp = (NullIfExpr *)node;
+
+ exp->depth = depth | (exp->depth & 1);
+
+ return false;
+ }
+ else if (IsA(node, RowCompareExpr))
+ {
+ RowCompareExpr *exp = (RowCompareExpr *)node;
+
+ exp->depth = depth | (exp->depth & 1);
+
+ return false;
+ }
+ return expression_tree_walker(node, mark_qualifiers_depth_walker, context);
+}
+
+static void
+mark_qualifiers_depth(Query *query, int depth)
+{
+ ListCell *l;
+
+ foreach (l, query->rtable)
+ {
+ RangeTblEntry *rte = lfirst(l);
+
+ /*
+ * If and when sub-query is defined as a security-barrier,
+ * any qualifiers of WHERE or JOIN ... ON clause must be
+ * launched earlier than ones come from upper nest level,
+ * even if the sub-query is enough simple to be pulled-up
+ * later, because user can reference contents of tuples to
+ * be invisible using functions with side-effect and much
+ * smaller cost estimation.
+ * The reason why depth should be incremented by 2, not 1,
+ * is that the least bit is reserved to launch particular
+ * functions earlier than others, even if same depth.
+ * We assume this mechanism is used to row-level security
+ * policy functions.
+ */
+ if (rte->rtekind == RTE_SUBQUERY)
+ {
+ if (rte->security_barrier)
+ mark_qualifiers_depth(rte->subquery, depth + 2);
+ else
+ mark_qualifiers_depth(rte->subquery, depth);
+ }
+ }
+ mark_qualifiers_depth_walker((Node *)query->jointree, &depth);
+}
/*
* expression_planner
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index be0935d..95c07aa 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -111,6 +111,7 @@ static Expr *simplify_function(Expr *oldexpr, Oid funcid,
Oid input_collid, List **args,
bool has_named_args,
bool allow_inline,
+ int depth,
eval_const_expressions_context *context);
static List *reorder_function_arguments(List *args, Oid result_type,
HeapTuple func_tuple,
@@ -123,7 +124,7 @@ static void recheck_cast_function_args(List *args, Oid result_type,
HeapTuple func_tuple);
static Expr *evaluate_function(Oid funcid, Oid result_type, int32 result_typmod,
Oid result_collid, Oid input_collid, List *args,
- HeapTuple func_tuple,
+ HeapTuple func_tuple, int depth,
eval_const_expressions_context *context);
static Expr *inline_function(Oid funcid, Oid result_type, Oid result_collid,
Oid input_collid, List *args,
@@ -2229,7 +2230,7 @@ eval_const_expressions_mutator(Node *node,
expr->funccollid,
expr->inputcollid,
&args,
- has_named_args, true, context);
+ has_named_args, true, expr->depth, context);
if (simple) /* successfully simplified it */
return (Node *) simple;
@@ -2248,6 +2249,7 @@ eval_const_expressions_mutator(Node *node,
newexpr->inputcollid = expr->inputcollid;
newexpr->args = args;
newexpr->location = expr->location;
+ newexpr->depth = expr->depth;
return (Node *) newexpr;
}
if (IsA(node, OpExpr))
@@ -2282,7 +2284,7 @@ eval_const_expressions_mutator(Node *node,
expr->opcollid,
expr->inputcollid,
&args,
- false, true, context);
+ false, true, expr->depth, context);
if (simple) /* successfully simplified it */
return (Node *) simple;
@@ -2313,6 +2315,7 @@ eval_const_expressions_mutator(Node *node,
newexpr->inputcollid = expr->inputcollid;
newexpr->args = args;
newexpr->location = expr->location;
+ newexpr->depth = expr->depth;
return (Node *) newexpr;
}
if (IsA(node, DistinctExpr))
@@ -2380,7 +2383,7 @@ eval_const_expressions_mutator(Node *node,
expr->opcollid,
expr->inputcollid,
&args,
- false, false, context);
+ false, false, expr->depth, context);
if (simple) /* successfully simplified it */
{
/*
@@ -2410,6 +2413,7 @@ eval_const_expressions_mutator(Node *node,
newexpr->inputcollid = expr->inputcollid;
newexpr->args = args;
newexpr->location = expr->location;
+ newexpr->depth = expr->depth;
return (Node *) newexpr;
}
if (IsA(node, BoolExpr))
@@ -2570,7 +2574,7 @@ eval_const_expressions_mutator(Node *node,
InvalidOid,
InvalidOid,
&args,
- false, true, context);
+ false, true, expr->depth, context);
if (simple) /* successfully simplified output fn */
{
/*
@@ -2591,7 +2595,7 @@ eval_const_expressions_mutator(Node *node,
expr->resultcollid,
InvalidOid,
&args,
- false, true, context);
+ false, true, expr->depth, context);
if (simple) /* successfully simplified input fn */
return (Node *) simple;
}
@@ -2607,6 +2611,7 @@ eval_const_expressions_mutator(Node *node,
newexpr->resultcollid = expr->resultcollid;
newexpr->coerceformat = expr->coerceformat;
newexpr->location = expr->location;
+ newexpr->depth = expr->depth;
return (Node *) newexpr;
}
if (IsA(node, ArrayCoerceExpr))
@@ -2631,6 +2636,7 @@ eval_const_expressions_mutator(Node *node,
newexpr->isExplicit = expr->isExplicit;
newexpr->coerceformat = expr->coerceformat;
newexpr->location = expr->location;
+ newexpr->depth = expr->depth;
/*
* If constant argument and it's a binary-coercible or immutable
@@ -3447,6 +3453,7 @@ simplify_function(Expr *oldexpr, Oid funcid,
Oid input_collid, List **args,
bool has_named_args,
bool allow_inline,
+ int depth,
eval_const_expressions_context *context)
{
HeapTuple func_tuple;
@@ -3477,7 +3484,7 @@ simplify_function(Expr *oldexpr, Oid funcid,
newexpr = evaluate_function(funcid, result_type, result_typmod,
result_collid, input_collid, *args,
- func_tuple, context);
+ func_tuple, depth, context);
/*
* Some functions calls can be simplified at plan time based on properties
@@ -3764,7 +3771,7 @@ recheck_cast_function_args(List *args, Oid result_type, HeapTuple func_tuple)
static Expr *
evaluate_function(Oid funcid, Oid result_type, int32 result_typmod,
Oid result_collid, Oid input_collid, List *args,
- HeapTuple func_tuple,
+ HeapTuple func_tuple, int depth,
eval_const_expressions_context *context)
{
Form_pg_proc funcform = (Form_pg_proc) GETSTRUCT(func_tuple);
@@ -3850,6 +3857,7 @@ evaluate_function(Oid funcid, Oid result_type, int32 result_typmod,
newexpr->inputcollid = input_collid;
newexpr->args = args;
newexpr->location = -1;
+ newexpr->depth = depth;
return evaluate_expr((Expr *) newexpr, result_type, result_typmod,
result_collid);
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index f1e20ef..b626386 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -335,6 +335,7 @@ typedef struct FuncExpr
Oid inputcollid; /* OID of collation that function should use */
List *args; /* arguments to the function */
int location; /* token location, or -1 if unknown */
+ int depth; /* depth of clause in the original query */
} FuncExpr;
/*
@@ -380,6 +381,7 @@ typedef struct OpExpr
Oid inputcollid; /* OID of collation that operator should use */
List *args; /* arguments to the operator (1 or 2) */
int location; /* token location, or -1 if unknown */
+ int depth; /* depth of clause in the original query */
} OpExpr;
/*
@@ -421,6 +423,7 @@ typedef struct ScalarArrayOpExpr
Oid inputcollid; /* OID of collation that operator should use */
List *args; /* the scalar and array operands */
int location; /* token location, or -1 if unknown */
+ int depth; /* depth of clause in the original query */
} ScalarArrayOpExpr;
/*
@@ -685,6 +688,7 @@ typedef struct CoerceViaIO
Oid resultcollid; /* OID of collation, or InvalidOid if none */
CoercionForm coerceformat; /* how to display this node */
int location; /* token location, or -1 if unknown */
+ int depth; /* depth of clause in the original query */
} CoerceViaIO;
/* ----------------
@@ -710,6 +714,7 @@ typedef struct ArrayCoerceExpr
bool isExplicit; /* conversion semantics flag to pass to func */
CoercionForm coerceformat; /* how to display this node */
int location; /* token location, or -1 if unknown */
+ int depth; /* depth of clause in the original query */
} ArrayCoerceExpr;
/* ----------------
@@ -901,6 +906,7 @@ typedef struct RowCompareExpr
List *inputcollids; /* OID list of collations for comparisons */
List *largs; /* the left-hand input arguments */
List *rargs; /* the right-hand input arguments */
+ int depth; /* depth of clause in the original query */
} RowCompareExpr;
/*
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index f659269..250191b 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -44,6 +44,7 @@ typedef struct QualCost
{
Cost startup; /* one-time cost */
Cost per_tuple; /* per-evaluation cost */
+ int depth; /* depth of qual in the original query */
} QualCost;
/*
diff --git a/src/test/regress/expected/select_views.out b/src/test/regress/expected/select_views.out
index 6cd317c..d1179d2 100644
--- a/src/test/regress/expected/select_views.out
+++ b/src/test/regress/expected/select_views.out
@@ -467,6 +467,20 @@ SELECT name, #thepath FROM iexit ORDER BY 1, 2;
I- 580 | 21
I- 580 | 22
I- 580 | 22
+ I- 580/I-680 Ramp | 2
+ I- 580/I-680 Ramp | 2
+ I- 580/I-680 Ramp | 2
+ I- 580/I-680 Ramp | 2
+ I- 580/I-680 Ramp | 2
+ I- 580/I-680 Ramp | 2
+ I- 580/I-680 Ramp | 4
+ I- 580/I-680 Ramp | 4
+ I- 580/I-680 Ramp | 4
+ I- 580/I-680 Ramp | 4
+ I- 580/I-680 Ramp | 5
+ I- 580/I-680 Ramp | 6
+ I- 580/I-680 Ramp | 6
+ I- 580/I-680 Ramp | 6
I- 580 Ramp | 2
I- 580 Ramp | 2
I- 580 Ramp | 2
@@ -717,20 +731,6 @@ SELECT name, #thepath FROM iexit ORDER BY 1, 2;
I- 580 Ramp | 8
I- 580 Ramp | 8
I- 580 Ramp | 8
- I- 580/I-680 Ramp | 2
- I- 580/I-680 Ramp | 2
- I- 580/I-680 Ramp | 2
- I- 580/I-680 Ramp | 2
- I- 580/I-680 Ramp | 2
- I- 580/I-680 Ramp | 2
- I- 580/I-680 Ramp | 4
- I- 580/I-680 Ramp | 4
- I- 580/I-680 Ramp | 4
- I- 580/I-680 Ramp | 4
- I- 580/I-680 Ramp | 5
- I- 580/I-680 Ramp | 6
- I- 580/I-680 Ramp | 6
- I- 580/I-680 Ramp | 6
I- 680 | 2
I- 680 | 2
I- 680 | 2
@@ -1247,3 +1247,66 @@ SELECT * FROM toyemp WHERE name = 'sharon';
sharon | 25 | (15,12) | 12000
(1 row)
+--
+-- Test for leaky-view
+--
+CREATE USER alice;
+CREATE FUNCTION f_leak(text, text)
+ RETURNS bool LANGUAGE 'plpgsql'
+ COST 0.00000001
+ AS 'begin raise notice ''% => %'', $1, $2; return true; end';
+CREATE TABLE credit_cards (
+ name text,
+ number text,
+ expired text
+);
+INSERT INTO credit_cards VALUES ('alice', '1111-2222-3333-4444', 'Aug-2012'),
+ ('bob', '5555-6666-7777-8888', 'Nov-2016'),
+ ('eve', '9801-2345-6789-0123', 'Jan-2018');
+CREATE VIEW your_credit_normal AS
+ SELECT * FROM credit_cards WHERE name = getpgusername();
+CREATE VIEW your_credit_secure WITH (security_barrier) AS
+ SELECT * FROM credit_cards WHERE name = getpgusername();
+GRANT SELECT ON your_credit_normal TO public;
+GRANT SELECT ON your_credit_secure TO public;
+-- run leaky view
+SET SESSION AUTHORIZATION alice;
+SELECT * FROM your_credit_normal WHERE f_leak(number,expired);
+NOTICE: 1111-2222-3333-4444 => Aug-2012
+NOTICE: 5555-6666-7777-8888 => Nov-2016
+NOTICE: 9801-2345-6789-0123 => Jan-2018
+ name | number | expired
+-------+---------------------+----------
+ alice | 1111-2222-3333-4444 | Aug-2012
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM your_credit_normal WHERE f_leak(number,expired);
+ QUERY PLAN
+--------------------------------------------------------------------------
+ Seq Scan on credit_cards
+ Filter: (f_leak(number, expired) AND (name = (getpgusername())::text))
+(2 rows)
+
+SELECT * FROM your_credit_secure WHERE f_leak(number,expired);
+NOTICE: 1111-2222-3333-4444 => Aug-2012
+ name | number | expired
+-------+---------------------+----------
+ alice | 1111-2222-3333-4444 | Aug-2012
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM your_credit_secure WHERE f_leak(number,expired);
+ QUERY PLAN
+--------------------------------------------------------------------------
+ Seq Scan on credit_cards
+ Filter: ((name = (getpgusername())::text) AND f_leak(number, expired))
+(2 rows)
+
+\c -
+-- cleanups
+DROP ROLE IF EXISTS alice;
+DROP FUNCTION IF EXISTS f_leak(text);
+NOTICE: function f_leak(text) does not exist, skipping
+DROP TABLE IF EXISTS credit_cards CASCADE;
+NOTICE: drop cascades to 2 other objects
+DETAIL: drop cascades to view your_credit_normal
+drop cascades to view your_credit_secure
diff --git a/src/test/regress/sql/select_views.sql b/src/test/regress/sql/select_views.sql
index 14f1be8..dc49601 100644
--- a/src/test/regress/sql/select_views.sql
+++ b/src/test/regress/sql/select_views.sql
@@ -8,3 +8,42 @@ SELECT * FROM street;
SELECT name, #thepath FROM iexit ORDER BY 1, 2;
SELECT * FROM toyemp WHERE name = 'sharon';
+
+--
+-- Test for leaky-view
+--
+
+CREATE USER alice;
+CREATE FUNCTION f_leak(text, text)
+ RETURNS bool LANGUAGE 'plpgsql'
+ COST 0.00000001
+ AS 'begin raise notice ''% => %'', $1, $2; return true; end';
+CREATE TABLE credit_cards (
+ name text,
+ number text,
+ expired text
+);
+INSERT INTO credit_cards VALUES ('alice', '1111-2222-3333-4444', 'Aug-2012'),
+ ('bob', '5555-6666-7777-8888', 'Nov-2016'),
+ ('eve', '9801-2345-6789-0123', 'Jan-2018');
+CREATE VIEW your_credit_normal AS
+ SELECT * FROM credit_cards WHERE name = getpgusername();
+CREATE VIEW your_credit_secure WITH (security_barrier) AS
+ SELECT * FROM credit_cards WHERE name = getpgusername();
+
+GRANT SELECT ON your_credit_normal TO public;
+GRANT SELECT ON your_credit_secure TO public;
+-- run leaky view
+SET SESSION AUTHORIZATION alice;
+
+SELECT * FROM your_credit_normal WHERE f_leak(number,expired);
+EXPLAIN (COSTS OFF) SELECT * FROM your_credit_normal WHERE f_leak(number,expired);
+
+SELECT * FROM your_credit_secure WHERE f_leak(number,expired);
+EXPLAIN (COSTS OFF) SELECT * FROM your_credit_secure WHERE f_leak(number,expired);
+
+\c -
+-- cleanups
+DROP ROLE IF EXISTS alice;
+DROP FUNCTION IF EXISTS f_leak(text);
+DROP TABLE IF EXISTS credit_cards CASCADE;
The simplified version of fix-leaky-view patch. The part of reloptions
for views got splitted out
into the part-0 patch, so it needs to be applied prior to this patch.
Rest of logic to prevent unexpected pushing down across security
barrier is not changed.
Thanks,
2011/6/6 Kohei Kaigai <Kohei.Kaigai@emea.nec.com>:
This patch enables to fix up leaky-view problem using qualifiers that reference only one-side of join-loop inside of view definition.
The point of this scenario is criteria to distribute qualifiers of scanning-plan distributed in distribute_qual_to_rels(). If and when a qualifiers that reference only one-side of join-loop, the optimizer may distribute this qualifier into inside of the join-loop, even if it goes over the boundary of a subquery expanded from a view for row-level security.
This behavior allows us to reference whole of one-side of join-loop using functions with side-effects.
The solution is quite simple; it prohibits to distribute qualifiers over the boundary of subquery, however, performance cost is unignorable, because it also disables to utilize obviously indexable qualifiers such as (id=123), so this patch requires users a hint whether a particular view is for row-level security, or not.This patch newly adds "CREATE SECURITY VIEW" statement that marks a flag to show this view was defined for row-level security purpose. This flag shall be stored as reloptions.
If this flag was set, the optimizer does not distribute qualifiers over the boundary of subqueries expanded from security views, except for obviously safe qualifiers.
(Right now, we consider built-in indexable operators are safe, but it might be arguable.)It fixes up the scenario [2] in the bellow descriprions.
--------
The background of the leaky-view problem is well summarized at:
http://wiki.postgresql.org/wiki/RLSWe had discussed several scenarios in v9.1 development cycle, and the last developer meeting. We almost concluded the following criteria to characterize whether a leak-view scenario is problematic to be fixed, or not.
* If unprived user can directly reference contents of invisible tuples, it is a problem to be fixed.
* As long as contents of invisible tuples are consumed by internal stuff (eg, index-access method), it is not a problem to be fixed.Thus, the scenario [1] and [2] are problematic to be fixed, but [3] and [4] are not. So, I'll try to fix up these two scenario with the patch part-1 amd part-2.
[1] unexpected reorder of functions with tiny-cost and side-effects
Qualifiers of WHERE or JOIN ... IN clause shall be sorted by estimated cost, not depth of nest level. Thus, this logic can make order reversal when user-given qualifier has smaller cost than qualifiers to perform as security policy inside of view.
In the result, these qualifiers can reference both of visible and invisible tuples prior to the filtering by row-level security policy of the view. Thus, this behavior can be used to leak contents of invisible tuples.[2] unexpected push-down of functions with side-effect into join-loop
If arguments of qualifier being appended on outside of join-loop references only one-side of the join-loop, it is a good strategy to distribute this qualifier into inside of the join-loop to minimize number of tuples to be joined, from the viewpoint of performance.
However, it also makes order reversal when the join-loop is a part of view definition that should perform row-level security policy. Then, these exogenetic qualifiers may be executed prior to the filtering by row-level security policy of the view. Thus, this behavior can be used to leak contents of invisible tuple.[3] estimation of hidden value using iteration of PK/FK proves
Due to the nature of PK/FK constraints, we can infer existence of key values being stored within invisible tuple, even if we never allows users to reference contents of invisible tuples.
We commonly call this type of information leaks "covert-channel", and it is basically impossible to prevent according to the previous security research, however, its risk is also relatively small because of slow bandwidth to leak.
We already made consensus this scenario is not a problem to be fixed.[4] estimation of hidden value using statistics
One example was selectivity-estimator function; that may reference statistical information delivered from the tables have invisible tuples for optimization. Here are two points to be considered. The one is purely internal stuff may be able to reference invisible tuples, however, it is not a problem as long as it does not leak them into end-users; such as index access methods. The second is statistical or other form of date delivered from invisible tuples. We can set up a table that contains data delivered from invisible tuples using row-level triggers, however, it is quite a matter of database administration. Unless owner of tables set up such a leakable configuration, other users cannot reference them.
Thanks,
--
NEC Europe Ltd, SAP Global Competence Center
KaiGai Kohei <kohei.kaigai@emea.nec.com>--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
Attachments:
pgsql-v9.2-fix-leaky-view-part-2.v3.patchapplication/octet-stream; name=pgsql-v9.2-fix-leaky-view-part-2.v3.patchDownload
doc/src/sgml/rules.sgml | 66 ++++++++++++
src/backend/nodes/copyfuncs.c | 1 +
src/backend/nodes/equalfuncs.c | 1 +
src/backend/nodes/outfuncs.c | 1 +
src/backend/nodes/readfuncs.c | 1 +
src/backend/optimizer/plan/initsplan.c | 96 ++++++++++++++----
src/backend/optimizer/prep/prepjointree.c | 7 ++
src/backend/optimizer/util/clauses.c | 113 ++++++++++++++++++++
src/backend/utils/cache/lsyscache.c | 19 ++++
src/include/nodes/primnodes.h | 1 +
src/include/optimizer/clauses.h | 1 +
src/include/utils/lsyscache.h | 1 +
src/test/regress/expected/select_views.out | 153 +++++++++++++++++++++++++---
src/test/regress/sql/select_views.sql | 67 ++++++++++++
14 files changed, 493 insertions(+), 35 deletions(-)
diff --git a/doc/src/sgml/rules.sgml b/doc/src/sgml/rules.sgml
index 1b06519..1b5ae6f 100644
--- a/doc/src/sgml/rules.sgml
+++ b/doc/src/sgml/rules.sgml
@@ -1856,6 +1856,72 @@ SELECT * FROM phone_number WHERE tricky(person, phone);
</para>
<para>
+ In addition, you might be able to leak contents of invisible tuples
+ using the following scenario:
+<programlisting>
+CREATE VIEW your_credit AS
+ SELECT a.rolname, c.number, c.expire
+ FROM pg_authid a JOIN credit_cards c ON a.oid = c.id
+ WHERE a.rolname = getpgusername();
+</programlisting>
+ This view also might seem secure, since any <command>SELECT</command>
+ from <literal>your_credit</literal> shall be rewritten into a
+ <command>SELECT</command> from the join of <literal>pg_authid</>
+ and <literal>credit_cards</> with a qualifier that filters out
+ any entries except for your credit card number.
+
+ But if a user appends his or her own functions that references
+ only columns come from a particular side of join loop, the optimizer
+ shall relocate this qualifier into the most deep level, independent
+ from cost estimation of the function.
+<programlisting>
+postgres=> SELECT * FROM your_credit WHERE tricky(number, expire);
+NOTICE: 1111-2222-3333-4444 => Jan-01
+NOTICE: 5555-6666-7777-8888 => Feb-02
+NOTICE: 1234-5678-9012-3456 => Mar-03
+ rolname | number | expire
+---------+---------------------+--------
+ alice | 5555-6666-7777-8888 | Feb-02
+(1 row)
+</programlisting>
+ The reason is obvious from the result of <command>EXPLAIN</command>.
+<programlisting>
+postgres=> EXPLAIN SELECT * FROM your_credit WHERE tricky(number, expire);
+ QUERY PLAN
+------------------------------------------------------------------------
+ Hash Join (cost=1.03..20.38 rows=1 width=128)
+ Hash Cond: (c.id = a.oid)
+ -> Seq Scan on credit_cards c (cost=0.00..18.30 rows=277 width=68)
+ Filter: tricky(number, expire)
+ -> Hash (cost=1.01..1.01 rows=1 width=68)
+ -> Seq Scan on pg_authid a (cost=0.00..1.01 rows=1 width=68)
+ Filter: (rolname = getpgusername())
+(7 rows)
+</programlisting>
+ The supplied <function>tricky</function> only references
+ <literal>number</literal> and <literal>expire</literal> columns,
+ however, the qualifier to filter invisible tuples performs on
+ the scan of <literal>pg_authid</literal>.
+ Then, since the optimizer tries to minimize the number of tuples
+ being joined, the supplied qualifer got attached on the scan of
+ <literal>credit_cards</literal>.
+ In the result, it allows <function>tricky</function> to reference
+ contents of the <literal>credit_cards</literal> table.
+</para>
+<para>
+ The <literal>security_berrier</literal> option of views enables
+ to prevent both of the scenarios, instead of a bit performance
+ trade-off. If and when a particular view is defined with
+ <literal>security_berrier=TRUE</literal>, any exogenetic qualifiers
+ cannot be pushed-down except for a limited number of trusted
+ operators being transformed into index scan, even if the supplied
+ qualifier references only one-side of relation joins. In addition,
+ underlying qualifiers of security barrier view shall be launched
+ earlier than others, even if sub-queries are pulled-up and
+ qualifiers got merged due to the optimization.
+</para>
+
+<para>
Similar considerations apply to update rules. In the examples of
the previous section, the owner of the tables in the example
database could grant the privileges <literal>SELECT</>,
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 4fb60a1..2af5113 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -1789,6 +1789,7 @@ _copyFromExpr(FromExpr *from)
COPY_NODE_FIELD(fromlist);
COPY_NODE_FIELD(quals);
+ COPY_SCALAR_FIELD(security_barrier);
return newnode;
}
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index d201f22..4e001f7 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -772,6 +772,7 @@ _equalFromExpr(FromExpr *a, FromExpr *b)
{
COMPARE_NODE_FIELD(fromlist);
COMPARE_NODE_FIELD(quals);
+ COMPARE_SCALAR_FIELD(security_barrier);
return true;
}
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 04a8760..3a263aa 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -1447,6 +1447,7 @@ _outFromExpr(StringInfo str, FromExpr *node)
WRITE_NODE_FIELD(fromlist);
WRITE_NODE_FIELD(quals);
+ WRITE_BOOL_FIELD(security_barrier);
}
/*****************************************************************************
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 098f3c3..b7fcb25 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -1161,6 +1161,7 @@ _readFromExpr(void)
READ_NODE_FIELD(fromlist);
READ_NODE_FIELD(quals);
+ READ_BOOL_FIELD(security_barrier);
READ_DONE();
}
diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c
index 333ede2..cd8c499 100644
--- a/src/backend/optimizer/plan/initsplan.c
+++ b/src/backend/optimizer/plan/initsplan.c
@@ -41,7 +41,8 @@ int join_collapse_limit;
static List *deconstruct_recurse(PlannerInfo *root, Node *jtnode,
bool below_outer_join,
- Relids *qualscope, Relids *inner_join_rels);
+ Relids *qualscope, Relids *inner_join_rels,
+ bool below_sec_barriers, Relids *sec_barriers);
static SpecialJoinInfo *make_outerjoininfo(PlannerInfo *root,
Relids left_rels, Relids right_rels,
Relids inner_join_rels,
@@ -52,7 +53,8 @@ static void distribute_qual_to_rels(PlannerInfo *root, Node *clause,
JoinType jointype,
Relids qualscope,
Relids ojscope,
- Relids outerjoin_nonnullable);
+ Relids outerjoin_nonnullable,
+ Relids sec_barriers);
static bool check_outerjoin_delay(PlannerInfo *root, Relids *relids_p,
Relids *nullable_relids_p, bool is_pushed_down);
static bool check_redundant_nullability_qual(PlannerInfo *root, Node *clause);
@@ -240,13 +242,15 @@ deconstruct_jointree(PlannerInfo *root)
{
Relids qualscope;
Relids inner_join_rels;
+ Relids sec_barriers;
+ FromExpr *f = (FromExpr *)root->parse->jointree;
/* Start recursion at top of jointree */
- Assert(root->parse->jointree != NULL &&
- IsA(root->parse->jointree, FromExpr));
+ Assert(root->parse->jointree != NULL && IsA(f, FromExpr));
- return deconstruct_recurse(root, (Node *) root->parse->jointree, false,
- &qualscope, &inner_join_rels);
+ return deconstruct_recurse(root, (Node *) f, false,
+ &qualscope, &inner_join_rels,
+ f->security_barrier, &sec_barriers);
}
/*
@@ -270,7 +274,8 @@ deconstruct_jointree(PlannerInfo *root)
*/
static List *
deconstruct_recurse(PlannerInfo *root, Node *jtnode, bool below_outer_join,
- Relids *qualscope, Relids *inner_join_rels)
+ Relids *qualscope, Relids *inner_join_rels,
+ bool below_sec_barriers, Relids *sec_barriers)
{
List *joinlist;
@@ -289,6 +294,9 @@ deconstruct_recurse(PlannerInfo *root, Node *jtnode, bool below_outer_join,
/* A single baserel does not create an inner join */
*inner_join_rels = NULL;
joinlist = list_make1(jtnode);
+ /* Is it in security barrier? */
+ *sec_barriers = (below_sec_barriers ?
+ bms_make_singleton(varno) : NULL);
}
else if (IsA(jtnode, FromExpr))
{
@@ -304,6 +312,7 @@ deconstruct_recurse(PlannerInfo *root, Node *jtnode, bool below_outer_join,
*/
*qualscope = NULL;
*inner_join_rels = NULL;
+ *sec_barriers = NULL;
joinlist = NIL;
remaining = list_length(f->fromlist);
foreach(l, f->fromlist)
@@ -311,12 +320,17 @@ deconstruct_recurse(PlannerInfo *root, Node *jtnode, bool below_outer_join,
Relids sub_qualscope;
List *sub_joinlist;
int sub_members;
+ Relids sub_barriers;
sub_joinlist = deconstruct_recurse(root, lfirst(l),
below_outer_join,
&sub_qualscope,
- inner_join_rels);
+ inner_join_rels,
+ below_sec_barriers ?
+ true : f->security_barrier,
+ &sub_barriers);
*qualscope = bms_add_members(*qualscope, sub_qualscope);
+ *sec_barriers = bms_add_members(*sec_barriers, sub_barriers);
sub_members = list_length(sub_joinlist);
remaining--;
if (sub_members <= 1 ||
@@ -345,7 +359,7 @@ deconstruct_recurse(PlannerInfo *root, Node *jtnode, bool below_outer_join,
distribute_qual_to_rels(root, qual,
false, below_outer_join, JOIN_INNER,
- *qualscope, NULL, NULL);
+ *qualscope, NULL, NULL, *sec_barriers);
}
}
else if (IsA(jtnode, JoinExpr))
@@ -355,6 +369,8 @@ deconstruct_recurse(PlannerInfo *root, Node *jtnode, bool below_outer_join,
rightids,
left_inners,
right_inners,
+ left_barriers,
+ right_barriers,
nonnullable_rels,
ojscope;
List *leftjoinlist,
@@ -379,12 +395,17 @@ deconstruct_recurse(PlannerInfo *root, Node *jtnode, bool below_outer_join,
case JOIN_INNER:
leftjoinlist = deconstruct_recurse(root, j->larg,
below_outer_join,
- &leftids, &left_inners);
+ &leftids, &left_inners,
+ below_sec_barriers,
+ &left_barriers);
rightjoinlist = deconstruct_recurse(root, j->rarg,
below_outer_join,
- &rightids, &right_inners);
+ &rightids, &right_inners,
+ below_sec_barriers,
+ &right_barriers);
*qualscope = bms_union(leftids, rightids);
*inner_join_rels = *qualscope;
+ *sec_barriers = bms_union(left_barriers, right_barriers);
/* Inner join adds no restrictions for quals */
nonnullable_rels = NULL;
break;
@@ -392,35 +413,50 @@ deconstruct_recurse(PlannerInfo *root, Node *jtnode, bool below_outer_join,
case JOIN_ANTI:
leftjoinlist = deconstruct_recurse(root, j->larg,
below_outer_join,
- &leftids, &left_inners);
+ &leftids, &left_inners,
+ below_sec_barriers,
+ &left_barriers);
rightjoinlist = deconstruct_recurse(root, j->rarg,
true,
- &rightids, &right_inners);
+ &rightids, &right_inners,
+ below_sec_barriers,
+ &right_barriers);
*qualscope = bms_union(leftids, rightids);
*inner_join_rels = bms_union(left_inners, right_inners);
+ *sec_barriers = bms_union(left_barriers, right_barriers);
nonnullable_rels = leftids;
break;
case JOIN_SEMI:
leftjoinlist = deconstruct_recurse(root, j->larg,
below_outer_join,
- &leftids, &left_inners);
+ &leftids, &left_inners,
+ below_sec_barriers,
+ &left_barriers);
rightjoinlist = deconstruct_recurse(root, j->rarg,
below_outer_join,
- &rightids, &right_inners);
+ &rightids, &right_inners,
+ below_sec_barriers,
+ &right_barriers);
*qualscope = bms_union(leftids, rightids);
*inner_join_rels = bms_union(left_inners, right_inners);
+ *sec_barriers = bms_union(left_barriers, right_barriers);
/* Semi join adds no restrictions for quals */
nonnullable_rels = NULL;
break;
case JOIN_FULL:
leftjoinlist = deconstruct_recurse(root, j->larg,
true,
- &leftids, &left_inners);
+ &leftids, &left_inners,
+ below_sec_barriers,
+ &left_barriers);
rightjoinlist = deconstruct_recurse(root, j->rarg,
true,
- &rightids, &right_inners);
+ &rightids, &right_inners,
+ below_sec_barriers,
+ &right_barriers);
*qualscope = bms_union(leftids, rightids);
*inner_join_rels = bms_union(left_inners, right_inners);
+ *sec_barriers = bms_union(left_barriers, right_barriers);
/* each side is both outer and inner */
nonnullable_rels = *qualscope;
break;
@@ -469,7 +505,8 @@ deconstruct_recurse(PlannerInfo *root, Node *jtnode, bool below_outer_join,
distribute_qual_to_rels(root, qual,
false, below_outer_join, j->jointype,
*qualscope,
- ojscope, nonnullable_rels);
+ ojscope, nonnullable_rels,
+ *sec_barriers);
}
/* Now we can add the SpecialJoinInfo to join_info_list */
@@ -793,7 +830,8 @@ distribute_qual_to_rels(PlannerInfo *root, Node *clause,
JoinType jointype,
Relids qualscope,
Relids ojscope,
- Relids outerjoin_nonnullable)
+ Relids outerjoin_nonnullable,
+ Relids sec_barriers)
{
Relids relids;
bool is_pushed_down;
@@ -801,6 +839,7 @@ distribute_qual_to_rels(PlannerInfo *root, Node *clause,
bool pseudoconstant = false;
bool maybe_equivalence;
bool maybe_outer_join;
+ bool maybe_leakable_clause = false;
Relids nullable_relids;
RestrictInfo *restrictinfo;
@@ -873,6 +912,21 @@ distribute_qual_to_rels(PlannerInfo *root, Node *clause,
}
}
+ /*
+ * If and when the supplied clause contains a leakable functions,
+ * it might be used to bypass row-level security using views.
+ * In this case, we should not push down the clause to prevent
+ * the leakable clause being evaluated prior to row-level policy
+ * functions.
+ */
+ if (!bms_is_empty(sec_barriers) &&
+ contain_leakable_functions(clause) &&
+ bms_overlap(relids, sec_barriers))
+ {
+ maybe_leakable_clause = true;
+ relids = bms_add_members(relids, sec_barriers);
+ }
+
/*----------
* Check to see if clause application must be delayed by outer-join
* considerations.
@@ -1075,7 +1129,7 @@ distribute_qual_to_rels(PlannerInfo *root, Node *clause,
* process_equivalence is successful, it will take care of that;
* otherwise, we have to call initialize_mergeclause_eclasses to do it.
*/
- if (restrictinfo->mergeopfamilies)
+ if (!maybe_leakable_clause && restrictinfo->mergeopfamilies)
{
if (maybe_equivalence)
{
@@ -1417,7 +1471,7 @@ process_implied_equality(PlannerInfo *root,
*/
distribute_qual_to_rels(root, (Node *) clause,
true, below_outer_join, JOIN_INNER,
- qualscope, NULL, NULL);
+ qualscope, NULL, NULL, NULL);
}
/*
diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
index ac622a3..ae96b4f 100644
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -705,6 +705,13 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte,
pull_up_subqueries(subroot, (Node *) subquery->jointree, NULL, NULL);
/*
+ * If and when the sub-query was originally defined as a view with
+ * "security_barrier" option, we need to mark this FromExpr as a
+ * security barrier to prevent unexpected distribution of qualifiers.
+ */
+ ((FromExpr *)subquery->jointree)->security_barrier = rte->security_barrier;
+
+ /*
* Now we must recheck whether the subquery is still simple enough to pull
* up. If not, abandon processing it.
*
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index be0935d..ddd1b0e 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -93,6 +93,7 @@ static bool contain_subplans_walker(Node *node, void *context);
static bool contain_mutable_functions_walker(Node *node, void *context);
static bool contain_volatile_functions_walker(Node *node, void *context);
static bool contain_nonstrict_functions_walker(Node *node, void *context);
+static bool contain_leakable_functions_walker(Node *node, void *context);
static Relids find_nonnullable_rels_walker(Node *node, bool top_level);
static List *find_nonnullable_vars_walker(Node *node, bool top_level);
static bool is_strict_saop(ScalarArrayOpExpr *expr, bool falseOK);
@@ -1164,6 +1165,118 @@ contain_nonstrict_functions_walker(Node *node, void *context)
context);
}
+/*****************************************************************************
+ * Check clauses for leakable functions
+ *****************************************************************************/
+
+/*
+ * contain_leakable_functions
+ * Recursively search for leakable functions within a clause.
+ *
+ * Returns true if any function call with side-effect is found.
+ * ie, some type-input/output handler will raise an error when given
+ * argument does not have a valid format.
+ *
+ * When people uses views for row-level security purpose, given qualifiers
+ * come from outside of the view should not be pushed down into the views
+ * if they have side-effect, because contents of tuples to be filtered out
+ * may be leaked via side-effectable functions within the qualifiers.
+ *
+ * The idea here is that the planner restrains a part of optimization when
+ * the qualifiers contains leakable functions.
+ * This routine checks whether the given clause contains leakable functions,
+ * or not. If we return false, then the clause is clean.
+ */
+bool
+contain_leakable_functions(Node *clause)
+{
+ return contain_leakable_functions_walker(clause, NULL);
+}
+
+static bool
+contain_leakable_functions_walker(Node *node, void *context)
+{
+ if (node == NULL)
+ return false;
+
+ if (IsA(node, FuncExpr))
+ {
+ /*
+ * Right now, we have no way to distinguish safe functions with
+ * leakable ones, so, we treat all the function call possibly
+ * leakable.
+ */
+ return true;
+ }
+ else if (IsA(node, OpExpr))
+ {
+ OpExpr *expr = (OpExpr *) node;
+
+ /*
+ * Right now, we assume operators implemented by built-in functions
+ * are not leakable, so it does not need to prevent optimization.
+ */
+ set_opfuncid(expr);
+ if (get_func_lang(expr->opfuncid) != INTERNALlanguageId)
+ return true;
+ /* else fall through to check args */
+ }
+ else if (IsA(node, DistinctExpr))
+ {
+ DistinctExpr *expr = (DistinctExpr *) node;
+
+ set_opfuncid((OpExpr *) expr);
+ if (get_func_lang(expr->opfuncid) != INTERNALlanguageId)
+ return true;
+ /* else fall through to check args */
+ }
+ else if (IsA(node, ScalarArrayOpExpr))
+ {
+ ScalarArrayOpExpr *expr = (ScalarArrayOpExpr *) node;
+
+ set_sa_opfuncid(expr);
+ if (get_func_lang(expr->opfuncid) != INTERNALlanguageId)
+ return true;
+ /* else fall through to check args */
+ }
+ else if (IsA(node, CoerceViaIO) ||
+ IsA(node, ArrayCoerceExpr))
+ {
+ /*
+ * we assume type-in/out handlers are leakable, even if built-in
+ * functions.
+ * ie, int4in() raises an error message with given argument,
+ * if it does not have valid format for numeric value.
+ */
+ return true;
+ }
+ else if (IsA(node, NullIfExpr))
+ {
+ NullIfExpr *expr = (NullIfExpr *) node;
+
+ set_opfuncid((OpExpr *) expr); /* rely on struct equivalence */
+ if (get_func_lang(expr->opfuncid) != INTERNALlanguageId)
+ return true;
+ /* else fall through to check args */
+ }
+ else if (IsA(node, RowCompareExpr))
+ {
+ /* RowCompare probably can't have volatile ops, but check anyway */
+ RowCompareExpr *rcexpr = (RowCompareExpr *) node;
+ ListCell *opid;
+
+ foreach(opid, rcexpr->opnos)
+ {
+ Oid funcId = get_opcode(lfirst_oid(opid));
+
+ if (get_func_lang(funcId) != INTERNALlanguageId)
+ return true;
+ }
+ /* else fall through to check args */
+ }
+ return expression_tree_walker(node, contain_leakable_functions_walker,
+ context);
+}
/*
* find_nonnullable_rels
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index 28d18b0..55571f1 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -1387,6 +1387,25 @@ get_func_namespace(Oid funcid)
}
/*
+ * get_func_lang
+ * Given procedure id, return the function's language
+ */
+Oid
+get_func_lang(Oid funcid)
+{
+ HeapTuple tp;
+ Oid result;
+
+ tp = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcid));
+ if (!HeapTupleIsValid(tp))
+ elog(ERROR, "cache lookup failed for function %u", funcid);
+
+ result = ((Form_pg_proc) GETSTRUCT(tp))->prolang;
+ ReleaseSysCache(tp);
+ return result;
+}
+
+/*
* get_func_rettype
* Given procedure id, return the function's result type.
*/
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index f1e20ef..cfef93f 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -1259,6 +1259,7 @@ typedef struct FromExpr
NodeTag type;
List *fromlist; /* List of join subtrees */
Node *quals; /* qualifiers on join, if any */
+ bool security_barrier; /* Come from security-barrier view? */
} FromExpr;
#endif /* PRIMNODES_H */
diff --git a/src/include/optimizer/clauses.h b/src/include/optimizer/clauses.h
index dde6d82..09cf54f 100644
--- a/src/include/optimizer/clauses.h
+++ b/src/include/optimizer/clauses.h
@@ -62,6 +62,7 @@ extern bool contain_subplans(Node *clause);
extern bool contain_mutable_functions(Node *clause);
extern bool contain_volatile_functions(Node *clause);
extern bool contain_nonstrict_functions(Node *clause);
+extern bool contain_leakable_functions(Node *clause);
extern Relids find_nonnullable_rels(Node *clause);
extern List *find_nonnullable_vars(Node *clause);
extern List *find_forced_null_vars(Node *clause);
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index 0a419dc..659cdc0 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -79,6 +79,7 @@ extern RegProcedure get_oprrest(Oid opno);
extern RegProcedure get_oprjoin(Oid opno);
extern char *get_func_name(Oid funcid);
extern Oid get_func_namespace(Oid funcid);
+extern Oid get_func_lang(Oid funcid);
extern Oid get_func_rettype(Oid funcid);
extern int get_func_nargs(Oid funcid);
extern Oid get_func_signature(Oid funcid, Oid **argtypes, int *nargs);
diff --git a/src/test/regress/expected/select_views.out b/src/test/regress/expected/select_views.out
index 6cd317c..5375aec 100644
--- a/src/test/regress/expected/select_views.out
+++ b/src/test/regress/expected/select_views.out
@@ -467,6 +467,20 @@ SELECT name, #thepath FROM iexit ORDER BY 1, 2;
I- 580 | 21
I- 580 | 22
I- 580 | 22
+ I- 580/I-680 Ramp | 2
+ I- 580/I-680 Ramp | 2
+ I- 580/I-680 Ramp | 2
+ I- 580/I-680 Ramp | 2
+ I- 580/I-680 Ramp | 2
+ I- 580/I-680 Ramp | 2
+ I- 580/I-680 Ramp | 4
+ I- 580/I-680 Ramp | 4
+ I- 580/I-680 Ramp | 4
+ I- 580/I-680 Ramp | 4
+ I- 580/I-680 Ramp | 5
+ I- 580/I-680 Ramp | 6
+ I- 580/I-680 Ramp | 6
+ I- 580/I-680 Ramp | 6
I- 580 Ramp | 2
I- 580 Ramp | 2
I- 580 Ramp | 2
@@ -717,20 +731,6 @@ SELECT name, #thepath FROM iexit ORDER BY 1, 2;
I- 580 Ramp | 8
I- 580 Ramp | 8
I- 580 Ramp | 8
- I- 580/I-680 Ramp | 2
- I- 580/I-680 Ramp | 2
- I- 580/I-680 Ramp | 2
- I- 580/I-680 Ramp | 2
- I- 580/I-680 Ramp | 2
- I- 580/I-680 Ramp | 2
- I- 580/I-680 Ramp | 4
- I- 580/I-680 Ramp | 4
- I- 580/I-680 Ramp | 4
- I- 580/I-680 Ramp | 4
- I- 580/I-680 Ramp | 5
- I- 580/I-680 Ramp | 6
- I- 580/I-680 Ramp | 6
- I- 580/I-680 Ramp | 6
I- 680 | 2
I- 680 | 2
I- 680 | 2
@@ -1247,3 +1247,128 @@ SELECT * FROM toyemp WHERE name = 'sharon';
sharon | 25 | (15,12) | 12000
(1 row)
+--
+-- Test for leaky-vew
+--
+CREATE USER alice;
+CREATE FUNCTION f_leak(text, text)
+ RETURNS bool LANGUAGE 'plpgsql'
+ COST 0.00000001
+ AS 'begin raise notice ''% => %'', $1, $2; return true; end';
+CREATE TABLE employee (
+ eid int primary key,
+ ename text,
+ etitle text
+);
+NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "employee_pkey" for table "employee"
+CREATE TABLE salary (
+ eid int references employee(eid),
+ salary int,
+ ymd date
+);
+CREATE INDEX salary_ymd on salary (ymd);
+INSERT INTO employee (eid, ename, etitle)
+ VALUES (100, 'alice', 'staff'),
+ (110, 'bob', 'manager'),
+ (120, 'eve', 'chief');
+INSERT INTO salary (eid, salary, ymd)
+ VALUES (100, 2000, '2011-06-01'),
+ (100, 2025, '2011-07-01'),
+ (110, 2500, '2011-06-01'),
+ (110, 2400, '2011-07-01'),
+ (120, 2200, '2011-07-01');
+CREATE VIEW my_salary_normal AS
+ SELECT ename,etitle,salary,ymd
+ FROM employee e JOIN salary s ON s.eid = s.eid
+ WHERE ename = getpgusername();
+CREATE VIEW my_salary_secure WITH (security_barrier) AS
+ SELECT ename,etitle,salary,ymd
+ FROM employee e JOIN salary s ON e.eid = s.eid
+ WHERE ename = getpgusername();
+GRANT SELECT ON my_salary_normal TO public;
+GRANT SELECT ON my_salary_secure TO public;
+-- run leaky view
+SET SESSION AUTHORIZATION alice;
+SELECT * FROM my_salary_normal
+ WHERE f_leak(ymd::text,salary::text);
+NOTICE: 06-01-2011 => 2000
+NOTICE: 07-01-2011 => 2025
+NOTICE: 06-01-2011 => 2500
+NOTICE: 07-01-2011 => 2400
+NOTICE: 07-01-2011 => 2200
+ ename | etitle | salary | ymd
+-------+--------+--------+------------
+ alice | staff | 2000 | 06-01-2011
+ alice | staff | 2025 | 07-01-2011
+ alice | staff | 2500 | 06-01-2011
+ alice | staff | 2400 | 07-01-2011
+ alice | staff | 2200 | 07-01-2011
+(5 rows)
+
+EXPLAIN (COSTS OFF) SELECT * FROM my_salary_normal
+ WHERE f_leak(ymd::text,salary::text);
+ QUERY PLAN
+-----------------------------------------------------------------------------
+ Nested Loop
+ -> Seq Scan on employee e
+ Filter: (ename = (getpgusername())::text)
+ -> Materialize
+ -> Seq Scan on salary s
+ Filter: ((eid = eid) AND f_leak((ymd)::text, (salary)::text))
+(6 rows)
+
+SELECT * FROM my_salary_secure
+ WHERE f_leak(ymd::text,salary::text);
+NOTICE: 06-01-2011 => 2000
+NOTICE: 07-01-2011 => 2025
+ ename | etitle | salary | ymd
+-------+--------+--------+------------
+ alice | staff | 2000 | 06-01-2011
+ alice | staff | 2025 | 07-01-2011
+(2 rows)
+
+EXPLAIN (COSTS OFF) SELECT * FROM my_salary_secure
+ WHERE f_leak(ymd::text,salary::text);
+ QUERY PLAN
+--------------------------------------------------------------------------------------------------
+ Hash Join
+ Hash Cond: (s.eid = e.eid)
+ Join Filter: ((e.ename = (getpgusername())::text) AND f_leak((s.ymd)::text, (s.salary)::text))
+ -> Seq Scan on salary s
+ -> Hash
+ -> Seq Scan on employee e
+(6 rows)
+
+SELECT * FROM my_salary_secure
+ WHERE f_leak(ymd::text,salary::text) AND ymd = '2011-06-01';
+NOTICE: 06-01-2011 => 2000
+ ename | etitle | salary | ymd
+-------+--------+--------+------------
+ alice | staff | 2000 | 06-01-2011
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM my_salary_secure
+ WHERE f_leak(ymd::text,salary::text) AND ymd = '2011-06-01';
+ QUERY PLAN
+--------------------------------------------------------------------------------------------------
+ Hash Join
+ Hash Cond: (e.eid = s.eid)
+ Join Filter: ((e.ename = (getpgusername())::text) AND f_leak((s.ymd)::text, (s.salary)::text))
+ -> Seq Scan on employee e
+ -> Hash
+ -> Bitmap Heap Scan on salary s
+ Recheck Cond: (ymd = '06-01-2011'::date)
+ -> Bitmap Index Scan on salary_ymd
+ Index Cond: (ymd = '06-01-2011'::date)
+(9 rows)
+
+\c -
+-- cleanups
+DROP ROLE IF EXISTS alice;
+DROP FUNCTION IF EXISTS f_leak(text);
+NOTICE: function f_leak(text) does not exist, skipping
+DROP TABLE IF EXISTS employee CASCADE;
+NOTICE: drop cascades to 3 other objects
+DETAIL: drop cascades to view my_salary_normal
+drop cascades to constraint salary_eid_fkey on table salary
+drop cascades to view my_salary_secure
diff --git a/src/test/regress/sql/select_views.sql b/src/test/regress/sql/select_views.sql
index 14f1be8..61151bc 100644
--- a/src/test/regress/sql/select_views.sql
+++ b/src/test/regress/sql/select_views.sql
@@ -8,3 +8,70 @@ SELECT * FROM street;
SELECT name, #thepath FROM iexit ORDER BY 1, 2;
SELECT * FROM toyemp WHERE name = 'sharon';
+
+--
+-- Test for leaky-vew
+--
+
+CREATE USER alice;
+CREATE FUNCTION f_leak(text, text)
+ RETURNS bool LANGUAGE 'plpgsql'
+ COST 0.00000001
+ AS 'begin raise notice ''% => %'', $1, $2; return true; end';
+
+CREATE TABLE employee (
+ eid int primary key,
+ ename text,
+ etitle text
+);
+
+CREATE TABLE salary (
+ eid int references employee(eid),
+ salary int,
+ ymd date
+);
+CREATE INDEX salary_ymd on salary (ymd);
+
+INSERT INTO employee (eid, ename, etitle)
+ VALUES (100, 'alice', 'staff'),
+ (110, 'bob', 'manager'),
+ (120, 'eve', 'chief');
+INSERT INTO salary (eid, salary, ymd)
+ VALUES (100, 2000, '2011-06-01'),
+ (100, 2025, '2011-07-01'),
+ (110, 2500, '2011-06-01'),
+ (110, 2400, '2011-07-01'),
+ (120, 2200, '2011-07-01');
+CREATE VIEW my_salary_normal AS
+ SELECT ename,etitle,salary,ymd
+ FROM employee e JOIN salary s ON s.eid = s.eid
+ WHERE ename = getpgusername();
+CREATE VIEW my_salary_secure WITH (security_barrier) AS
+ SELECT ename,etitle,salary,ymd
+ FROM employee e JOIN salary s ON e.eid = s.eid
+ WHERE ename = getpgusername();
+
+GRANT SELECT ON my_salary_normal TO public;
+GRANT SELECT ON my_salary_secure TO public;
+-- run leaky view
+SET SESSION AUTHORIZATION alice;
+
+SELECT * FROM my_salary_normal
+ WHERE f_leak(ymd::text,salary::text);
+EXPLAIN (COSTS OFF) SELECT * FROM my_salary_normal
+ WHERE f_leak(ymd::text,salary::text);
+
+SELECT * FROM my_salary_secure
+ WHERE f_leak(ymd::text,salary::text);
+EXPLAIN (COSTS OFF) SELECT * FROM my_salary_secure
+ WHERE f_leak(ymd::text,salary::text);
+
+SELECT * FROM my_salary_secure
+ WHERE f_leak(ymd::text,salary::text) AND ymd = '2011-06-01';
+EXPLAIN (COSTS OFF) SELECT * FROM my_salary_secure
+ WHERE f_leak(ymd::text,salary::text) AND ymd = '2011-06-01';
+\c -
+-- cleanups
+DROP ROLE IF EXISTS alice;
+DROP FUNCTION IF EXISTS f_leak(text);
+DROP TABLE IF EXISTS employee CASCADE;
\ No newline at end of file
On Sun, Jul 03, 2011 at 11:33:38AM +0200, Kohei KaiGai wrote:
The attached patches are revised version.
The part-0 provides 'security_barrier' option for view definition, and performs
as a common basis of part-1 and part-2 patches.
Syntax is extended as follows:CREATE VIEW view_name [WITH (param [=value])] AS query;
We can also turn on/off this security_barrier setting by ALTER TABLE with
SET/RESET options.The part-1 patch enforces the qualifiers originally located under the security
barrier view to be launched prior to ones supplied on upper level.
The differences from the previous version is this barrier become conditional,
not always. So, existing optimization will be applied without any changes
onto non-security-barrier views.
I tested various query trees I considered interesting, and this version had
sound semantics for all of them. I have one suggestion for CREATE OR REPLACE
VIEW semantics, plus various cosmetic comments.
These patches are unified diffs, rather than project-standard context diffs.
Part 0:
--- a/doc/src/sgml/ref/create_view.sgml +++ b/doc/src/sgml/ref/create_view.sgml @@ -22,6 +22,7 @@ PostgreSQL documentation <refsynopsisdiv> <synopsis> CREATE [ OR REPLACE ] [ TEMP | TEMPORARY ] VIEW <replaceable class="PARAMETER">name</replaceable> [ ( <replaceable class="PARAMETER">column_name</replaceable> [, ...] ) ] + [ WITH ( parameter [= value] [, ... ] ) ]
This needs a bit more markup; see the corresponding case in create_table.sgml.
alter_view.sgml also needs an update. Incidentally, we should use ALTER VIEW
SET OPTION when referring to setting this for a view. ALTER TABLE SET OPTION
will also support views, since that's the general pattern for tablecmds.c type
checks, but that's largely an implementation detail.
--- a/src/backend/commands/view.c +++ b/src/backend/commands/view.c @@ -97,7 +97,8 @@ isViewOnTempTable_walker(Node *node, void *context) *--------------------------------------------------------------------- */ static Oid -DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace) +DefineVirtualRelation(const RangeVar *relation, List *tlist, + bool replace, List *options) { Oid viewOid, namespaceId;
This hunk and the hunk for the function's caller get rejects due to another
recent signature change.
@@ -167,6 +168,8 @@ DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace) { Relation rel; TupleDesc descriptor; + List *atcmds = NIL; + AlterTableCmd *atcmd;/*
* Yes. Get exclusive lock on the existing view ...
@@ -211,14 +214,11 @@ DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace)
*/
if (list_length(attrList) > rel->rd_att->natts)
{
- List *atcmds = NIL;
ListCell *c;
int skip = rel->rd_att->natts;foreach(c, attrList) { - AlterTableCmd *atcmd; - if (skip > 0) { skip--; @@ -229,10 +229,24 @@ DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace) atcmd->def = (Node *) lfirst(c); atcmds = lappend(atcmds, atcmd); } - AlterTableInternal(viewOid, atcmds, true); }/* + * If optional parameters are specified, we must set options + * using ALTER TABLE SET OPTION internally.
I think CREATE OR REPLACE VIEW should replace the option list, while ALTER
VIEW SET OPTION should retain its current behavior. That is, this should
leave the view with no options set:
create or replace view v0(n) with (security_barrier) as values (1), (2), (3), (4);
select reloptions from pg_class where oid = 'v0'::regclass;
create or replace view v0(n) as values (4), (3), (2), (1);
select reloptions from pg_class where oid = 'v0'::regclass;
+ */ + if (list_length(options) > 0) + { + atcmd = makeNode(AlterTableCmd); + atcmd->subtype = AT_SetRelOptions; + atcmd->def = options;
This line produces a warning:
view.c: In function `DefineVirtualRelation':
view.c:240: warning: assignment from incompatible pointer type
+ + atcmds = lappend(atcmds, atcmd); + } + if (atcmds != NIL) + AlterTableInternal(viewOid, atcmds, true); + + /* * Seems okay, so return the OID of the pre-existing view. */ relation_close(rel, NoLock); /* keep the lock! */
Part 1:
--- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c
@@ -2993,6 +3001,131 @@ get_column_info_for_window(PlannerInfo *root, WindowClause *wc, List *tlist,
}
}+/* + * mark_qualifiers_depth + * + * It marks depth field of the each expression nodes that eventually + * invokes functions, to track the original nest-level. On the evaluation + * of qualifiers within WHERE or JOIN ... ON clauses during relation scans, + * these items shall be reordered according to the nest-level and estimated + * cost. + * The optimizer may pull-up simple sub-queries or join clause, and + * qualifiers to filter out tuples shall be mixed with ones in upper- + * level. Thus, we need to track the original nest-level of qualifiers + * to prevent reverse of order in evaluation, because some of qualifiers + * can have side-effects that allows to leak supplied argument to outside. + * It can be abused to break row-level security using a user defined function + * with very small estimated cost, so nest level of qualifiers originated + * from is used as a criteria, rather than estimated cost, to decide order + * to evaluate qualifiers. + */ +static bool +mark_qualifiers_depth_walker(Node *node, void *context) +{ + int depth = *((int *)(context)); + + if (node == NULL) + return false; + if (IsA(node, FuncExpr)) + { + FuncExpr *exp = (FuncExpr *)node; + + exp->depth = depth | (exp->depth & 1);
Why did these change from plain "exp->depth = depth;" of the last version?
Since no core code sets a 1-bit in a depth value, I assume it must be related
to your future-use design for that bit. If so: could an external module
realistically take advantage of this? If yes, then a mere comment is in
order. If not, I think we should remove this (and the incrementing by 2) and
add it again in the future patch that makes use thereof.
--- a/src/test/regress/sql/select_views.sql +++ b/src/test/regress/sql/select_views.sql @@ -8,3 +8,42 @@ SELECT * FROM street; SELECT name, #thepath FROM iexit ORDER BY 1, 2;SELECT * FROM toyemp WHERE name = 'sharon'; + +-- +-- Test for leaky-view +-- + +CREATE USER alice; +CREATE FUNCTION f_leak(text, text) + RETURNS bool LANGUAGE 'plpgsql' + COST 0.00000001 + AS 'begin raise notice ''% => %'', $1, $2; return true; end'; +CREATE TABLE credit_cards ( + name text, + number text, + expired text +); +INSERT INTO credit_cards VALUES ('alice', '1111-2222-3333-4444', 'Aug-2012'), + ('bob', '5555-6666-7777-8888', 'Nov-2016'), + ('eve', '9801-2345-6789-0123', 'Jan-2018'); +CREATE VIEW your_credit_normal AS + SELECT * FROM credit_cards WHERE name = getpgusername(); +CREATE VIEW your_credit_secure WITH (security_barrier) AS + SELECT * FROM credit_cards WHERE name = getpgusername(); + +GRANT SELECT ON your_credit_normal TO public; +GRANT SELECT ON your_credit_secure TO public; +-- run leaky view +SET SESSION AUTHORIZATION alice; + +SELECT * FROM your_credit_normal WHERE f_leak(number,expired); +EXPLAIN (COSTS OFF) SELECT * FROM your_credit_normal WHERE f_leak(number,expired); + +SELECT * FROM your_credit_secure WHERE f_leak(number,expired); +EXPLAIN (COSTS OFF) SELECT * FROM your_credit_secure WHERE f_leak(number,expired); + +\c - +-- cleanups +DROP ROLE IF EXISTS alice; +DROP FUNCTION IF EXISTS f_leak(text); +DROP TABLE IF EXISTS credit_cards CASCADE;
Keep the view around. That way, pg_dump tests of the regression database will
test the dumping of this view option. (Your pg_dump support for this feature
does work fine, though.)
Thanks,
nm
Thanks for your detailed viewing and testing.
The attached patches are revised version.
Part-0)
* The patch was re-generated using context diff, instead of unified diff
* Documentation on ALTER VIEW was added
* Behavior of CREATE OR REPLACE VIEW was revised; when we replace
an existing view, reloption shall be reset, even if a particular
value was set.
* And, cosmetic changes; eliminate warnings due to lack of type cast.
Part-1)
* I removed the code to increment depth by 2, and preserve the latest bit,
because we have no module to utilize this mechanism right now.
Thanks,
2011/7/5 Noah Misch <noah@2ndquadrant.com>:
On Sun, Jul 03, 2011 at 11:33:38AM +0200, Kohei KaiGai wrote:
The attached patches are revised version.
The part-0 provides 'security_barrier' option for view definition, and performs
as a common basis of part-1 and part-2 patches.
Syntax is extended as follows:CREATE VIEW view_name [WITH (param [=value])] AS query;
We can also turn on/off this security_barrier setting by ALTER TABLE with
SET/RESET options.The part-1 patch enforces the qualifiers originally located under the security
barrier view to be launched prior to ones supplied on upper level.
The differences from the previous version is this barrier become conditional,
not always. So, existing optimization will be applied without any changes
onto non-security-barrier views.I tested various query trees I considered interesting, and this version had
sound semantics for all of them. I have one suggestion for CREATE OR REPLACE
VIEW semantics, plus various cosmetic comments.These patches are unified diffs, rather than project-standard context diffs.
Part 0:
--- a/doc/src/sgml/ref/create_view.sgml +++ b/doc/src/sgml/ref/create_view.sgml @@ -22,6 +22,7 @@ PostgreSQL documentation <refsynopsisdiv> <synopsis> CREATE [ OR REPLACE ] [ TEMP | TEMPORARY ] VIEW <replaceable class="PARAMETER">name</replaceable> [ ( <replaceable class="PARAMETER">column_name</replaceable> [, ...] ) ] + [ WITH ( parameter [= value] [, ... ] ) ]This needs a bit more markup; see the corresponding case in create_table.sgml.
alter_view.sgml also needs an update. Incidentally, we should use ALTER VIEW
SET OPTION when referring to setting this for a view. ALTER TABLE SET OPTION
will also support views, since that's the general pattern for tablecmds.c type
checks, but that's largely an implementation detail.--- a/src/backend/commands/view.c +++ b/src/backend/commands/view.c @@ -97,7 +97,8 @@ isViewOnTempTable_walker(Node *node, void *context) *--------------------------------------------------------------------- */ static Oid -DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace) +DefineVirtualRelation(const RangeVar *relation, List *tlist, + bool replace, List *options) { Oid viewOid, namespaceId;This hunk and the hunk for the function's caller get rejects due to another
recent signature change.@@ -167,6 +168,8 @@ DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace) { Relation rel; TupleDesc descriptor; + List *atcmds = NIL; + AlterTableCmd *atcmd;/* * Yes. Get exclusive lock on the existing view ... @@ -211,14 +214,11 @@ DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace) */ if (list_length(attrList) > rel->rd_att->natts) { - List *atcmds = NIL; ListCell *c; int skip = rel->rd_att->natts;foreach(c, attrList) { - AlterTableCmd *atcmd; - if (skip > 0) { skip--; @@ -229,10 +229,24 @@ DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace) atcmd->def = (Node *) lfirst(c); atcmds = lappend(atcmds, atcmd); } - AlterTableInternal(viewOid, atcmds, true); }/* + * If optional parameters are specified, we must set options + * using ALTER TABLE SET OPTION internally.I think CREATE OR REPLACE VIEW should replace the option list, while ALTER
VIEW SET OPTION should retain its current behavior. That is, this should
leave the view with no options set:create or replace view v0(n) with (security_barrier) as values (1), (2), (3), (4);
select reloptions from pg_class where oid = 'v0'::regclass;
create or replace view v0(n) as values (4), (3), (2), (1);
select reloptions from pg_class where oid = 'v0'::regclass;+ */ + if (list_length(options) > 0) + { + atcmd = makeNode(AlterTableCmd); + atcmd->subtype = AT_SetRelOptions; + atcmd->def = options;This line produces a warning:
view.c: In function `DefineVirtualRelation':
view.c:240: warning: assignment from incompatible pointer type+ + atcmds = lappend(atcmds, atcmd); + } + if (atcmds != NIL) + AlterTableInternal(viewOid, atcmds, true); + + /* * Seems okay, so return the OID of the pre-existing view. */ relation_close(rel, NoLock); /* keep the lock! */Part 1:
--- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c@@ -2993,6 +3001,131 @@ get_column_info_for_window(PlannerInfo *root, WindowClause *wc, List *tlist,
}
}+/* + * mark_qualifiers_depth + * + * It marks depth field of the each expression nodes that eventually + * invokes functions, to track the original nest-level. On the evaluation + * of qualifiers within WHERE or JOIN ... ON clauses during relation scans, + * these items shall be reordered according to the nest-level and estimated + * cost. + * The optimizer may pull-up simple sub-queries or join clause, and + * qualifiers to filter out tuples shall be mixed with ones in upper- + * level. Thus, we need to track the original nest-level of qualifiers + * to prevent reverse of order in evaluation, because some of qualifiers + * can have side-effects that allows to leak supplied argument to outside. + * It can be abused to break row-level security using a user defined function + * with very small estimated cost, so nest level of qualifiers originated + * from is used as a criteria, rather than estimated cost, to decide order + * to evaluate qualifiers. + */ +static bool +mark_qualifiers_depth_walker(Node *node, void *context) +{ + int depth = *((int *)(context)); + + if (node == NULL) + return false; + if (IsA(node, FuncExpr)) + { + FuncExpr *exp = (FuncExpr *)node; + + exp->depth = depth | (exp->depth & 1);Why did these change from plain "exp->depth = depth;" of the last version?
Since no core code sets a 1-bit in a depth value, I assume it must be related
to your future-use design for that bit. If so: could an external module
realistically take advantage of this? If yes, then a mere comment is in
order. If not, I think we should remove this (and the incrementing by 2) and
add it again in the future patch that makes use thereof.--- a/src/test/regress/sql/select_views.sql +++ b/src/test/regress/sql/select_views.sql @@ -8,3 +8,42 @@ SELECT * FROM street; SELECT name, #thepath FROM iexit ORDER BY 1, 2;SELECT * FROM toyemp WHERE name = 'sharon'; + +-- +-- Test for leaky-view +-- + +CREATE USER alice; +CREATE FUNCTION f_leak(text, text) + RETURNS bool LANGUAGE 'plpgsql' + COST 0.00000001 + AS 'begin raise notice ''% => %'', $1, $2; return true; end'; +CREATE TABLE credit_cards ( + name text, + number text, + expired text +); +INSERT INTO credit_cards VALUES ('alice', '1111-2222-3333-4444', 'Aug-2012'), + ('bob', '5555-6666-7777-8888', 'Nov-2016'), + ('eve', '9801-2345-6789-0123', 'Jan-2018'); +CREATE VIEW your_credit_normal AS + SELECT * FROM credit_cards WHERE name = getpgusername(); +CREATE VIEW your_credit_secure WITH (security_barrier) AS + SELECT * FROM credit_cards WHERE name = getpgusername(); + +GRANT SELECT ON your_credit_normal TO public; +GRANT SELECT ON your_credit_secure TO public; +-- run leaky view +SET SESSION AUTHORIZATION alice; + +SELECT * FROM your_credit_normal WHERE f_leak(number,expired); +EXPLAIN (COSTS OFF) SELECT * FROM your_credit_normal WHERE f_leak(number,expired); + +SELECT * FROM your_credit_secure WHERE f_leak(number,expired); +EXPLAIN (COSTS OFF) SELECT * FROM your_credit_secure WHERE f_leak(number,expired); + +\c - +-- cleanups +DROP ROLE IF EXISTS alice; +DROP FUNCTION IF EXISTS f_leak(text); +DROP TABLE IF EXISTS credit_cards CASCADE;Keep the view around. That way, pg_dump tests of the regression database will
test the dumping of this view option. (Your pg_dump support for this feature
does work fine, though.)Thanks,
nm
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
Attachments:
pgsql-v9.2-fix-leaky-view-part-0.v4.patchapplication/octet-stream; name=pgsql-v9.2-fix-leaky-view-part-0.v4.patchDownload
*** a/doc/src/sgml/ref/alter_view.sgml
--- b/doc/src/sgml/ref/alter_view.sgml
***************
*** 26,31 **** ALTER VIEW <replaceable class="parameter">name</replaceable> ALTER [ COLUMN ] <r
--- 26,34 ----
ALTER VIEW <replaceable class="parameter">name</replaceable> OWNER TO <replaceable class="PARAMETER">new_owner</replaceable>
ALTER VIEW <replaceable class="parameter">name</replaceable> RENAME TO <replaceable class="parameter">new_name</replaceable>
ALTER VIEW <replaceable class="parameter">name</replaceable> SET SCHEMA <replaceable class="parameter">new_schema</replaceable>
+ ALTER VIEW <replaceable class="parameter">name</replaceable> SET ( <replaceable class="parameter">parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )
+ ALTER VIEW <replaceable class="parameter">name</replaceable> RESET ( <replaceable class="parameter">parameter</replaceable> [, ... ] )
+
</synopsis>
</refsynopsisdiv>
***************
*** 102,107 **** ALTER VIEW <replaceable class="parameter">name</replaceable> SET SCHEMA <replace
--- 105,128 ----
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><replaceable class="parameter">parameter</replaceable></term>
+ <listitem>
+ <para>
+ Name of the view option to be set.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><replaceable class="parameter">value</replaceable></term>
+ <listitem>
+ <para>
+ The new value for the view option.
+ </para>
+ </listitem>
+ </varlistentry>
</variablelist>
</refsect1>
*** a/doc/src/sgml/ref/create_view.sgml
--- b/doc/src/sgml/ref/create_view.sgml
***************
*** 22,27 **** PostgreSQL documentation
--- 22,28 ----
<refsynopsisdiv>
<synopsis>
CREATE [ OR REPLACE ] [ TEMP | TEMPORARY ] VIEW <replaceable class="PARAMETER">name</replaceable> [ ( <replaceable class="PARAMETER">column_name</replaceable> [, ...] ) ]
+ [ WITH ( <replaceable class="PARAMETER">parameter</replaceable> [= <replaceable class="PARAMETER">value</replaceable>] [, ... ] ) ]
AS <replaceable class="PARAMETER">query</replaceable>
</synopsis>
</refsynopsisdiv>
***************
*** 99,104 **** CREATE [ OR REPLACE ] [ TEMP | TEMPORARY ] VIEW <replaceable class="PARAMETER">n
--- 100,128 ----
</varlistentry>
<varlistentry>
+ <term><literal>WITH (<replaceable class="parameter">parameter [= value]</replaceable>)</literal></term>
+ <listitem>
+ <para>
+ This clause allows to specify optional parameters for a view.
+ </para>
+ <para>
+ If <literal>security_barrier=TRUE</literal> is specified, this view
+ shall performs as security barrier that prevent unexpected information
+ leaks. It is a recommendable configuration when the view is defined
+ to apply row-level security, in spite of performance trade-off.
+ </para>
+ <para>
+ It is a commonly-used technique that using views to filter out
+ tuple to be invisible to particular users, however, please note
+ that here is a known-problem that allows malicious users to
+ reference invisible tuples using a function with side-effect
+ because of interaction with query optimization.
+ See <xref linkend="rules-privileges"> for more detailed scenario.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
<term><replaceable class="parameter">query</replaceable></term>
<listitem>
<para>
*** a/src/backend/access/common/reloptions.c
--- b/src/backend/access/common/reloptions.c
***************
*** 66,71 **** static relopt_bool boolRelOpts[] =
--- 66,79 ----
},
true
},
+ {
+ {
+ "security_barrier",
+ "Prevent information leaks using functions with side-effects",
+ RELOPT_KIND_VIEW
+ },
+ false
+ },
/* list terminator */
{{NULL}}
};
***************
*** 776,781 **** extractRelOptions(HeapTuple tuple, TupleDesc tupdesc, Oid amoptions)
--- 784,790 ----
{
case RELKIND_RELATION:
case RELKIND_TOASTVALUE:
+ case RELKIND_VIEW:
case RELKIND_UNCATALOGED:
options = heap_reloptions(classForm->relkind, datum, false);
break;
***************
*** 1134,1140 **** default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
{"autovacuum_vacuum_scale_factor", RELOPT_TYPE_REAL,
offsetof(StdRdOptions, autovacuum) +offsetof(AutoVacOpts, vacuum_scale_factor)},
{"autovacuum_analyze_scale_factor", RELOPT_TYPE_REAL,
! offsetof(StdRdOptions, autovacuum) +offsetof(AutoVacOpts, analyze_scale_factor)}
};
options = parseRelOptions(reloptions, validate, kind, &numoptions);
--- 1143,1151 ----
{"autovacuum_vacuum_scale_factor", RELOPT_TYPE_REAL,
offsetof(StdRdOptions, autovacuum) +offsetof(AutoVacOpts, vacuum_scale_factor)},
{"autovacuum_analyze_scale_factor", RELOPT_TYPE_REAL,
! offsetof(StdRdOptions, autovacuum) +offsetof(AutoVacOpts, analyze_scale_factor)},
! {"security_barrier", RELOPT_TYPE_BOOL,
! offsetof(StdRdOptions, security_barrier)},
};
options = parseRelOptions(reloptions, validate, kind, &numoptions);
***************
*** 1176,1181 **** heap_reloptions(char relkind, Datum reloptions, bool validate)
--- 1187,1194 ----
return (bytea *) rdopts;
case RELKIND_RELATION:
return default_reloptions(reloptions, validate, RELOPT_KIND_HEAP);
+ case RELKIND_VIEW:
+ return default_reloptions(reloptions, validate, RELOPT_KIND_VIEW);
default:
/* other relkinds are not supported */
return NULL;
*** a/src/backend/commands/tablecmds.c
--- b/src/backend/commands/tablecmds.c
***************
*** 2955,2961 **** ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
break;
case AT_SetRelOptions: /* SET (...) */
case AT_ResetRelOptions: /* RESET (...) */
! ATSimplePermissions(rel, ATT_TABLE | ATT_INDEX);
/* This command never recurses */
/* No command-specific prep needed */
pass = AT_PASS_MISC;
--- 2955,2961 ----
break;
case AT_SetRelOptions: /* SET (...) */
case AT_ResetRelOptions: /* RESET (...) */
! ATSimplePermissions(rel, ATT_TABLE | ATT_INDEX | ATT_VIEW);
/* This command never recurses */
/* No command-specific prep needed */
pass = AT_PASS_MISC;
***************
*** 7911,7916 **** ATExecSetRelOptions(Relation rel, List *defList, bool isReset, LOCKMODE lockmode
--- 7911,7917 ----
{
case RELKIND_RELATION:
case RELKIND_TOASTVALUE:
+ case RELKIND_VIEW:
(void) heap_reloptions(rel->rd_rel->relkind, newOptions, true);
break;
case RELKIND_INDEX:
*** a/src/backend/commands/view.c
--- b/src/backend/commands/view.c
***************
*** 98,104 **** isViewOnTempTable_walker(Node *node, void *context)
*/
static Oid
DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace,
! Oid namespaceId)
{
Oid viewOid;
CreateStmt *createStmt = makeNode(CreateStmt);
--- 98,104 ----
*/
static Oid
DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace,
! Oid namespaceId, List *options)
{
Oid viewOid;
CreateStmt *createStmt = makeNode(CreateStmt);
***************
*** 166,171 **** DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace,
--- 166,173 ----
{
Relation rel;
TupleDesc descriptor;
+ List *atcmds = NIL;
+ AlterTableCmd *atcmd;
/*
* Yes. Get exclusive lock on the existing view ...
***************
*** 210,223 **** DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace,
*/
if (list_length(attrList) > rel->rd_att->natts)
{
- List *atcmds = NIL;
ListCell *c;
int skip = rel->rd_att->natts;
foreach(c, attrList)
{
- AlterTableCmd *atcmd;
-
if (skip > 0)
{
skip--;
--- 212,222 ----
***************
*** 228,237 **** DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace,
atcmd->def = (Node *) lfirst(c);
atcmds = lappend(atcmds, atcmd);
}
- AlterTableInternal(viewOid, atcmds, true);
}
/*
* Seems okay, so return the OID of the pre-existing view.
*/
relation_close(rel, NoLock); /* keep the lock! */
--- 227,257 ----
atcmd->def = (Node *) lfirst(c);
atcmds = lappend(atcmds, atcmd);
}
}
/*
+ * If optional parameters are specified, we must set options
+ * using ALTER TABLE SET OPTION internally.
+ */
+ if (list_length(options) > 0)
+ {
+ atcmd = makeNode(AlterTableCmd);
+ atcmd->subtype = AT_SetRelOptions;
+ atcmd->def = (List *)options;
+
+ atcmds = lappend(atcmds, atcmd);
+ }
+ else
+ {
+ atcmd = makeNode(AlterTableCmd);
+ atcmd->subtype = AT_ResetRelOptions;
+ atcmd->def = (Node *) list_make1(makeDefElem("security_barrier",
+ NULL));
+ }
+ if (atcmds != NIL)
+ AlterTableInternal(viewOid, atcmds, true);
+
+ /*
* Seems okay, so return the OID of the pre-existing view.
*/
relation_close(rel, NoLock); /* keep the lock! */
***************
*** 255,260 **** DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace,
--- 275,283 ----
createStmt->tablespacename = NULL;
createStmt->if_not_exists = false;
+ if (options != NIL)
+ createStmt->options = list_concat(createStmt->options, options);
+
/*
* finally create the relation (this will error out if there's an
* existing view, so we don't need more code to complain if "replace"
***************
*** 513,519 **** DefineView(ViewStmt *stmt, const char *queryString)
* aborted.
*/
viewOid = DefineVirtualRelation(view, viewParse->targetList,
! stmt->replace, namespaceId);
/*
* The relation we have just created is not visible to any other commands
--- 536,543 ----
* aborted.
*/
viewOid = DefineVirtualRelation(view, viewParse->targetList,
! stmt->replace, namespaceId,
! stmt->options);
/*
* The relation we have just created is not visible to any other commands
*** a/src/backend/nodes/copyfuncs.c
--- b/src/backend/nodes/copyfuncs.c
***************
*** 1944,1949 **** _copyRangeTblEntry(RangeTblEntry *from)
--- 1944,1950 ----
COPY_SCALAR_FIELD(relid);
COPY_SCALAR_FIELD(relkind);
COPY_NODE_FIELD(subquery);
+ COPY_SCALAR_FIELD(security_barrier);
COPY_SCALAR_FIELD(jointype);
COPY_NODE_FIELD(joinaliasvars);
COPY_NODE_FIELD(funcexpr);
*** a/src/backend/nodes/equalfuncs.c
--- b/src/backend/nodes/equalfuncs.c
***************
*** 2303,2308 **** _equalRangeTblEntry(RangeTblEntry *a, RangeTblEntry *b)
--- 2303,2309 ----
COMPARE_SCALAR_FIELD(relid);
COMPARE_SCALAR_FIELD(relkind);
COMPARE_NODE_FIELD(subquery);
+ COMPARE_SCALAR_FIELD(security_barrier);
COMPARE_SCALAR_FIELD(jointype);
COMPARE_NODE_FIELD(joinaliasvars);
COMPARE_NODE_FIELD(funcexpr);
*** a/src/backend/nodes/outfuncs.c
--- b/src/backend/nodes/outfuncs.c
***************
*** 2311,2316 **** _outRangeTblEntry(StringInfo str, RangeTblEntry *node)
--- 2311,2317 ----
break;
case RTE_SUBQUERY:
WRITE_NODE_FIELD(subquery);
+ WRITE_BOOL_FIELD(security_barrier);
break;
case RTE_JOIN:
WRITE_ENUM_FIELD(jointype, JoinType);
*** a/src/backend/nodes/readfuncs.c
--- b/src/backend/nodes/readfuncs.c
***************
*** 1191,1196 **** _readRangeTblEntry(void)
--- 1191,1197 ----
break;
case RTE_SUBQUERY:
READ_NODE_FIELD(subquery);
+ READ_BOOL_FIELD(security_barrier);
break;
case RTE_JOIN:
READ_ENUM_FIELD(jointype, JoinType);
*** a/src/backend/parser/gram.y
--- b/src/backend/parser/gram.y
***************
*** 7266,7291 **** transaction_mode_list_or_empty:
*
*****************************************************************************/
! ViewStmt: CREATE OptTemp VIEW qualified_name opt_column_list
AS SelectStmt opt_check_option
{
ViewStmt *n = makeNode(ViewStmt);
n->view = $4;
n->view->relpersistence = $2;
n->aliases = $5;
! n->query = $7;
n->replace = false;
$$ = (Node *) n;
}
! | CREATE OR REPLACE OptTemp VIEW qualified_name opt_column_list
AS SelectStmt opt_check_option
{
ViewStmt *n = makeNode(ViewStmt);
n->view = $6;
n->view->relpersistence = $4;
n->aliases = $7;
! n->query = $9;
n->replace = true;
$$ = (Node *) n;
}
;
--- 7266,7293 ----
*
*****************************************************************************/
! ViewStmt: CREATE OptTemp VIEW qualified_name opt_column_list opt_reloptions
AS SelectStmt opt_check_option
{
ViewStmt *n = makeNode(ViewStmt);
n->view = $4;
n->view->relpersistence = $2;
n->aliases = $5;
! n->query = $8;
n->replace = false;
+ n->options = $6;
$$ = (Node *) n;
}
! | CREATE OR REPLACE OptTemp VIEW qualified_name opt_column_list opt_reloptions
AS SelectStmt opt_check_option
{
ViewStmt *n = makeNode(ViewStmt);
n->view = $6;
n->view->relpersistence = $4;
n->aliases = $7;
! n->query = $10;
n->replace = true;
+ n->options = $8;
$$ = (Node *) n;
}
;
*** a/src/backend/rewrite/rewriteHandler.c
--- b/src/backend/rewrite/rewriteHandler.c
***************
*** 1383,1388 **** ApplyRetrieveRule(Query *parsetree,
--- 1383,1389 ----
rte->rtekind = RTE_SUBQUERY;
rte->relid = InvalidOid;
rte->subquery = rule_action;
+ rte->security_barrier = RelationIsSecurityView(relation);
rte->inh = false; /* must not be set for a subquery */
/*
*** a/src/backend/utils/cache/relcache.c
--- b/src/backend/utils/cache/relcache.c
***************
*** 377,382 **** RelationParseRelOptions(Relation relation, HeapTuple tuple)
--- 377,383 ----
case RELKIND_RELATION:
case RELKIND_TOASTVALUE:
case RELKIND_INDEX:
+ case RELKIND_VIEW:
break;
default:
return;
*** a/src/bin/pg_dump/pg_dump.c
--- b/src/bin/pg_dump/pg_dump.c
***************
*** 12023,12030 **** dumpTableSchema(Archive *fout, TableInfo *tbinfo)
if (binary_upgrade)
binary_upgrade_set_pg_class_oids(q, tbinfo->dobj.catId.oid, false);
! appendPQExpBuffer(q, "CREATE VIEW %s AS\n %s\n",
! fmtId(tbinfo->dobj.name), viewdef);
appendPQExpBuffer(labelq, "VIEW %s",
fmtId(tbinfo->dobj.name));
--- 12023,12032 ----
if (binary_upgrade)
binary_upgrade_set_pg_class_oids(q, tbinfo->dobj.catId.oid, false);
! appendPQExpBuffer(q, "CREATE VIEW %s", fmtId(tbinfo->dobj.name));
! if (tbinfo->reloptions && strlen(tbinfo->reloptions) > 0)
! appendPQExpBuffer(q, " WITH (%s)", tbinfo->reloptions);
! appendPQExpBuffer(q, " AS\n %s\n", viewdef);
appendPQExpBuffer(labelq, "VIEW %s",
fmtId(tbinfo->dobj.name));
*** a/src/include/access/reloptions.h
--- b/src/include/access/reloptions.h
***************
*** 42,49 **** typedef enum relopt_kind
RELOPT_KIND_GIST = (1 << 5),
RELOPT_KIND_ATTRIBUTE = (1 << 6),
RELOPT_KIND_TABLESPACE = (1 << 7),
/* if you add a new kind, make sure you update "last_default" too */
! RELOPT_KIND_LAST_DEFAULT = RELOPT_KIND_TABLESPACE,
/* some compilers treat enums as signed ints, so we can't use 1 << 31 */
RELOPT_KIND_MAX = (1 << 30)
} relopt_kind;
--- 42,50 ----
RELOPT_KIND_GIST = (1 << 5),
RELOPT_KIND_ATTRIBUTE = (1 << 6),
RELOPT_KIND_TABLESPACE = (1 << 7),
+ RELOPT_KIND_VIEW = (1 << 8),
/* if you add a new kind, make sure you update "last_default" too */
! RELOPT_KIND_LAST_DEFAULT = RELOPT_KIND_VIEW,
/* some compilers treat enums as signed ints, so we can't use 1 << 31 */
RELOPT_KIND_MAX = (1 << 30)
} relopt_kind;
*** a/src/include/nodes/parsenodes.h
--- b/src/include/nodes/parsenodes.h
***************
*** 706,711 **** typedef struct RangeTblEntry
--- 706,713 ----
*/
Query *subquery; /* the sub-query */
+ bool security_barrier; /* Was a security barrier view? */
+
/*
* Fields valid for a join RTE (else NULL/zero):
*
***************
*** 2339,2344 **** typedef struct ViewStmt
--- 2341,2347 ----
List *aliases; /* target column names */
Node *query; /* the SELECT query */
bool replace; /* replace an existing view? */
+ List *options; /* options from WITH clause */
} ViewStmt;
/* ----------------------
*** a/src/include/utils/rel.h
--- b/src/include/utils/rel.h
***************
*** 168,174 **** typedef struct RelationData
/*
* StdRdOptions
! * Standard contents of rd_options for heaps and generic indexes.
*
* RelationGetFillFactor() and RelationGetTargetPageFreeSpace() can only
* be applied to relations that use this format or a superset for
--- 168,174 ----
/*
* StdRdOptions
! * Standard contents of rd_options for heaps, views and generic indexes.
*
* RelationGetFillFactor() and RelationGetTargetPageFreeSpace() can only
* be applied to relations that use this format or a superset for
***************
*** 194,199 **** typedef struct StdRdOptions
--- 194,200 ----
int32 vl_len_; /* varlena header (do not touch directly!) */
int fillfactor; /* page fill factor in percent (0..100) */
AutoVacOpts autovacuum; /* autovacuum-related options */
+ bool security_barrier; /* performs as security-barrier view */
} StdRdOptions;
#define HEAP_MIN_FILLFACTOR 10
***************
*** 222,227 **** typedef struct StdRdOptions
--- 223,236 ----
(BLCKSZ * (100 - RelationGetFillFactor(relation, defaultff)) / 100)
/*
+ * RelationIsSecurityView
+ * Returns whether the relation is security view, or not
+ */
+ #define RelationIsSecurityView(relation) \
+ ((relation)->rd_options ? \
+ ((StdRdOptions *) (relation)->rd_options)->security_barrier : false)
+
+ /*
* RelationIsValid
* True iff relation descriptor is valid.
*/
*** a/src/test/regress/expected/create_view.out
--- b/src/test/regress/expected/create_view.out
***************
*** 239,244 **** And relnamespace IN (SELECT OID FROM pg_namespace WHERE nspname LIKE 'pg_temp%')
--- 239,272 ----
1
(1 row)
+ --Should work correctly to leaky-view scenario
+ CREATE TABLE lvtest1 (a int, b text);
+ CREATE TABLE lvtest2 (x int, y text);
+ INSERT INTO lvtest1 VALUES (10, 'aaa'), (11, 'bbb'), (12, 'ccc'), (13, 'ddd');
+ INSERT INTO lvtest2 VALUES (11, 'xxx'), (12, 'yyy'), (13, 'zzz'), (14, 'xyz');
+ CREATE OR REPLACE VIEW leaky_v1 WITH (security_barrier=true) AS
+ SELECT * FROM lvtest1 JOIN lvtest2 ON a = x WHERE a % 2 = 0; -- OK
+ CREATE OR REPLACE VIEW leaky_v2 WITH (security_barrier=false) AS
+ SELECT * FROM lvtest1 JOIN lvtest2 ON a = x WHERE a % 2 = 1; -- OK
+ CREATE OR REPLACE VIEW leaky_v3 WITH (invalid_option=1234) AS
+ SELECT * FROM lvtest1 JOIN lvtest2 ON a = x WHERE a % 2 = 1; -- Fail
+ ERROR: unrecognized parameter "invalid_option"
+ SELECT relname, reloptions FROM pg_class WHERE relname like 'leaky_v%';
+ relname | reloptions
+ ----------+--------------------------
+ leaky_v1 | {security_barrier=true}
+ leaky_v2 | {security_barrier=false}
+ (2 rows)
+
+ ALTER TABLE leaky_v1 RESET ( security_barrier );
+ ALTER TABLE leaky_v2 SET ( security_barrier );
+ SELECT relname, reloptions FROM pg_class WHERE relname like 'leaky_v%';
+ relname | reloptions
+ ----------+-------------------------
+ leaky_v1 |
+ leaky_v2 | {security_barrier=true}
+ (2 rows)
+
DROP SCHEMA temp_view_test CASCADE;
NOTICE: drop cascades to 22 other objects
DETAIL: drop cascades to table temp_view_test.base_table
***************
*** 264,270 **** drop cascades to view temp_view_test.v8
drop cascades to sequence temp_view_test.seq1
drop cascades to view temp_view_test.v9
DROP SCHEMA testviewschm2 CASCADE;
! NOTICE: drop cascades to 16 other objects
DETAIL: drop cascades to table t1
drop cascades to view temporal1
drop cascades to view temporal2
--- 292,298 ----
drop cascades to sequence temp_view_test.seq1
drop cascades to view temp_view_test.v9
DROP SCHEMA testviewschm2 CASCADE;
! NOTICE: drop cascades to 20 other objects
DETAIL: drop cascades to table t1
drop cascades to view temporal1
drop cascades to view temporal2
***************
*** 281,284 **** drop cascades to table tbl3
--- 309,316 ----
drop cascades to table tbl4
drop cascades to view mytempview
drop cascades to view pubview
+ drop cascades to table lvtest1
+ drop cascades to table lvtest2
+ drop cascades to view leaky_v1
+ drop cascades to view leaky_v2
SET search_path to public;
*** a/src/test/regress/sql/create_view.sql
--- b/src/test/regress/sql/create_view.sql
***************
*** 191,196 **** AND NOT EXISTS (SELECT g FROM tbl4 LEFT JOIN tmptbl ON tbl4.h = tmptbl.j);
--- 191,215 ----
SELECT count(*) FROM pg_class where relname LIKE 'mytempview'
And relnamespace IN (SELECT OID FROM pg_namespace WHERE nspname LIKE 'pg_temp%');
+ --Should work correctly to leaky-view scenario
+ CREATE TABLE lvtest1 (a int, b text);
+ CREATE TABLE lvtest2 (x int, y text);
+
+ INSERT INTO lvtest1 VALUES (10, 'aaa'), (11, 'bbb'), (12, 'ccc'), (13, 'ddd');
+ INSERT INTO lvtest2 VALUES (11, 'xxx'), (12, 'yyy'), (13, 'zzz'), (14, 'xyz');
+
+ CREATE OR REPLACE VIEW leaky_v1 WITH (security_barrier=true) AS
+ SELECT * FROM lvtest1 JOIN lvtest2 ON a = x WHERE a % 2 = 0; -- OK
+ CREATE OR REPLACE VIEW leaky_v2 WITH (security_barrier=false) AS
+ SELECT * FROM lvtest1 JOIN lvtest2 ON a = x WHERE a % 2 = 1; -- OK
+ CREATE OR REPLACE VIEW leaky_v3 WITH (invalid_option=1234) AS
+ SELECT * FROM lvtest1 JOIN lvtest2 ON a = x WHERE a % 2 = 1; -- Fail
+ SELECT relname, reloptions FROM pg_class WHERE relname like 'leaky_v%';
+
+ ALTER TABLE leaky_v1 RESET ( security_barrier );
+ ALTER TABLE leaky_v2 SET ( security_barrier );
+ SELECT relname, reloptions FROM pg_class WHERE relname like 'leaky_v%';
+
DROP SCHEMA temp_view_test CASCADE;
DROP SCHEMA testviewschm2 CASCADE;
pgsql-v9.2-fix-leaky-view-part-1.v4.patchapplication/octet-stream; name=pgsql-v9.2-fix-leaky-view-part-1.v4.patchDownload
*** a/src/backend/nodes/copyfuncs.c
--- b/src/backend/nodes/copyfuncs.c
***************
*** 1188,1193 **** _copyFuncExpr(FuncExpr *from)
--- 1188,1194 ----
COPY_SCALAR_FIELD(inputcollid);
COPY_NODE_FIELD(args);
COPY_LOCATION_FIELD(location);
+ COPY_SCALAR_FIELD(depth);
return newnode;
}
***************
*** 1224,1229 **** _copyOpExpr(OpExpr *from)
--- 1225,1231 ----
COPY_SCALAR_FIELD(inputcollid);
COPY_NODE_FIELD(args);
COPY_LOCATION_FIELD(location);
+ COPY_SCALAR_FIELD(depth);
return newnode;
}
***************
*** 1244,1249 **** _copyDistinctExpr(DistinctExpr *from)
--- 1246,1252 ----
COPY_SCALAR_FIELD(inputcollid);
COPY_NODE_FIELD(args);
COPY_LOCATION_FIELD(location);
+ COPY_SCALAR_FIELD(depth);
return newnode;
}
***************
*** 1264,1269 **** _copyNullIfExpr(NullIfExpr *from)
--- 1267,1273 ----
COPY_SCALAR_FIELD(inputcollid);
COPY_NODE_FIELD(args);
COPY_LOCATION_FIELD(location);
+ COPY_SCALAR_FIELD(depth);
return newnode;
}
***************
*** 1282,1287 **** _copyScalarArrayOpExpr(ScalarArrayOpExpr *from)
--- 1286,1292 ----
COPY_SCALAR_FIELD(inputcollid);
COPY_NODE_FIELD(args);
COPY_LOCATION_FIELD(location);
+ COPY_SCALAR_FIELD(depth);
return newnode;
}
***************
*** 1422,1427 **** _copyCoerceViaIO(CoerceViaIO *from)
--- 1427,1433 ----
COPY_SCALAR_FIELD(resultcollid);
COPY_SCALAR_FIELD(coerceformat);
COPY_LOCATION_FIELD(location);
+ COPY_SCALAR_FIELD(depth);
return newnode;
}
***************
*** 1442,1447 **** _copyArrayCoerceExpr(ArrayCoerceExpr *from)
--- 1448,1454 ----
COPY_SCALAR_FIELD(isExplicit);
COPY_SCALAR_FIELD(coerceformat);
COPY_LOCATION_FIELD(location);
+ COPY_SCALAR_FIELD(depth);
return newnode;
}
***************
*** 1574,1579 **** _copyRowCompareExpr(RowCompareExpr *from)
--- 1581,1587 ----
COPY_NODE_FIELD(inputcollids);
COPY_NODE_FIELD(largs);
COPY_NODE_FIELD(rargs);
+ COPY_SCALAR_FIELD(depth);
return newnode;
}
*** a/src/backend/nodes/equalfuncs.c
--- b/src/backend/nodes/equalfuncs.c
***************
*** 248,253 **** _equalFuncExpr(FuncExpr *a, FuncExpr *b)
--- 248,254 ----
COMPARE_SCALAR_FIELD(inputcollid);
COMPARE_NODE_FIELD(args);
COMPARE_LOCATION_FIELD(location);
+ COMPARE_SCALAR_FIELD(depth);
return true;
}
***************
*** 285,290 **** _equalOpExpr(OpExpr *a, OpExpr *b)
--- 286,292 ----
COMPARE_SCALAR_FIELD(inputcollid);
COMPARE_NODE_FIELD(args);
COMPARE_LOCATION_FIELD(location);
+ COMPARE_SCALAR_FIELD(depth);
return true;
}
***************
*** 311,316 **** _equalDistinctExpr(DistinctExpr *a, DistinctExpr *b)
--- 313,319 ----
COMPARE_SCALAR_FIELD(inputcollid);
COMPARE_NODE_FIELD(args);
COMPARE_LOCATION_FIELD(location);
+ COMPARE_SCALAR_FIELD(depth);
return true;
}
***************
*** 337,342 **** _equalNullIfExpr(NullIfExpr *a, NullIfExpr *b)
--- 340,346 ----
COMPARE_SCALAR_FIELD(inputcollid);
COMPARE_NODE_FIELD(args);
COMPARE_LOCATION_FIELD(location);
+ COMPARE_SCALAR_FIELD(depth);
return true;
}
***************
*** 361,366 **** _equalScalarArrayOpExpr(ScalarArrayOpExpr *a, ScalarArrayOpExpr *b)
--- 365,371 ----
COMPARE_SCALAR_FIELD(inputcollid);
COMPARE_NODE_FIELD(args);
COMPARE_LOCATION_FIELD(location);
+ COMPARE_SCALAR_FIELD(depth);
return true;
}
***************
*** 479,484 **** _equalCoerceViaIO(CoerceViaIO *a, CoerceViaIO *b)
--- 484,490 ----
return false;
COMPARE_LOCATION_FIELD(location);
+ COMPARE_SCALAR_FIELD(depth);
return true;
}
***************
*** 503,508 **** _equalArrayCoerceExpr(ArrayCoerceExpr *a, ArrayCoerceExpr *b)
--- 509,515 ----
return false;
COMPARE_LOCATION_FIELD(location);
+ COMPARE_SCALAR_FIELD(depth);
return true;
}
***************
*** 613,618 **** _equalRowCompareExpr(RowCompareExpr *a, RowCompareExpr *b)
--- 620,626 ----
COMPARE_NODE_FIELD(inputcollids);
COMPARE_NODE_FIELD(largs);
COMPARE_NODE_FIELD(rargs);
+ COMPARE_SCALAR_FIELD(depth);
return true;
}
*** a/src/backend/nodes/outfuncs.c
--- b/src/backend/nodes/outfuncs.c
***************
*** 1004,1009 **** _outFuncExpr(StringInfo str, FuncExpr *node)
--- 1004,1010 ----
WRITE_OID_FIELD(inputcollid);
WRITE_NODE_FIELD(args);
WRITE_LOCATION_FIELD(location);
+ WRITE_INT_FIELD(depth);
}
static void
***************
*** 1030,1035 **** _outOpExpr(StringInfo str, OpExpr *node)
--- 1031,1037 ----
WRITE_OID_FIELD(inputcollid);
WRITE_NODE_FIELD(args);
WRITE_LOCATION_FIELD(location);
+ WRITE_INT_FIELD(depth);
}
static void
***************
*** 1045,1050 **** _outDistinctExpr(StringInfo str, DistinctExpr *node)
--- 1047,1053 ----
WRITE_OID_FIELD(inputcollid);
WRITE_NODE_FIELD(args);
WRITE_LOCATION_FIELD(location);
+ WRITE_INT_FIELD(depth);
}
static void
***************
*** 1060,1065 **** _outNullIfExpr(StringInfo str, NullIfExpr *node)
--- 1063,1069 ----
WRITE_OID_FIELD(inputcollid);
WRITE_NODE_FIELD(args);
WRITE_LOCATION_FIELD(location);
+ WRITE_INT_FIELD(depth);
}
static void
***************
*** 1073,1078 **** _outScalarArrayOpExpr(StringInfo str, ScalarArrayOpExpr *node)
--- 1077,1083 ----
WRITE_OID_FIELD(inputcollid);
WRITE_NODE_FIELD(args);
WRITE_LOCATION_FIELD(location);
+ WRITE_INT_FIELD(depth);
}
static void
***************
*** 1190,1195 **** _outCoerceViaIO(StringInfo str, CoerceViaIO *node)
--- 1195,1201 ----
WRITE_OID_FIELD(resultcollid);
WRITE_ENUM_FIELD(coerceformat, CoercionForm);
WRITE_LOCATION_FIELD(location);
+ WRITE_INT_FIELD(depth);
}
static void
***************
*** 1205,1210 **** _outArrayCoerceExpr(StringInfo str, ArrayCoerceExpr *node)
--- 1211,1217 ----
WRITE_BOOL_FIELD(isExplicit);
WRITE_ENUM_FIELD(coerceformat, CoercionForm);
WRITE_LOCATION_FIELD(location);
+ WRITE_INT_FIELD(depth);
}
static void
***************
*** 1297,1302 **** _outRowCompareExpr(StringInfo str, RowCompareExpr *node)
--- 1304,1310 ----
WRITE_NODE_FIELD(inputcollids);
WRITE_NODE_FIELD(largs);
WRITE_NODE_FIELD(rargs);
+ WRITE_INT_FIELD(depth);
}
static void
*** a/src/backend/nodes/readfuncs.c
--- b/src/backend/nodes/readfuncs.c
***************
*** 541,546 **** _readFuncExpr(void)
--- 541,547 ----
READ_OID_FIELD(inputcollid);
READ_NODE_FIELD(args);
READ_LOCATION_FIELD(location);
+ READ_INT_FIELD(depth);
READ_DONE();
}
***************
*** 588,593 **** _readOpExpr(void)
--- 589,595 ----
READ_OID_FIELD(inputcollid);
READ_NODE_FIELD(args);
READ_LOCATION_FIELD(location);
+ READ_INT_FIELD(depth);
READ_DONE();
}
***************
*** 619,624 **** _readDistinctExpr(void)
--- 621,627 ----
READ_OID_FIELD(inputcollid);
READ_NODE_FIELD(args);
READ_LOCATION_FIELD(location);
+ READ_INT_FIELD(depth);
READ_DONE();
}
***************
*** 650,655 **** _readNullIfExpr(void)
--- 653,659 ----
READ_OID_FIELD(inputcollid);
READ_NODE_FIELD(args);
READ_LOCATION_FIELD(location);
+ READ_INT_FIELD(depth);
READ_DONE();
}
***************
*** 679,684 **** _readScalarArrayOpExpr(void)
--- 683,689 ----
READ_OID_FIELD(inputcollid);
READ_NODE_FIELD(args);
READ_LOCATION_FIELD(location);
+ READ_INT_FIELD(depth);
READ_DONE();
}
***************
*** 794,799 **** _readCoerceViaIO(void)
--- 799,805 ----
READ_OID_FIELD(resultcollid);
READ_ENUM_FIELD(coerceformat, CoercionForm);
READ_LOCATION_FIELD(location);
+ READ_INT_FIELD(depth);
READ_DONE();
}
***************
*** 814,819 **** _readArrayCoerceExpr(void)
--- 820,826 ----
READ_BOOL_FIELD(isExplicit);
READ_ENUM_FIELD(coerceformat, CoercionForm);
READ_LOCATION_FIELD(location);
+ READ_INT_FIELD(depth);
READ_DONE();
}
***************
*** 946,951 **** _readRowCompareExpr(void)
--- 953,959 ----
READ_NODE_FIELD(inputcollids);
READ_NODE_FIELD(largs);
READ_NODE_FIELD(rargs);
+ READ_INT_FIELD(depth);
READ_DONE();
}
*** a/src/backend/optimizer/path/costsize.c
--- b/src/backend/optimizer/path/costsize.c
***************
*** 2593,2598 **** cost_qual_eval(QualCost *cost, List *quals, PlannerInfo *root)
--- 2593,2599 ----
context.root = root;
context.total.startup = 0;
context.total.per_tuple = 0;
+ context.total.depth = 0;
/* We don't charge any cost for the implicit ANDing at top level ... */
***************
*** 2618,2623 **** cost_qual_eval_node(QualCost *cost, Node *qual, PlannerInfo *root)
--- 2619,2625 ----
context.root = root;
context.total.startup = 0;
context.total.per_tuple = 0;
+ context.total.depth = 0;
cost_qual_eval_walker(qual, &context);
***************
*** 2647,2652 **** cost_qual_eval_walker(Node *node, cost_qual_eval_context *context)
--- 2649,2655 ----
locContext.root = context->root;
locContext.total.startup = 0;
locContext.total.per_tuple = 0;
+ locContext.total.depth = 0;
/*
* For an OR clause, recurse into the marked-up tree so that we
***************
*** 2671,2676 **** cost_qual_eval_walker(Node *node, cost_qual_eval_context *context)
--- 2674,2681 ----
}
context->total.startup += rinfo->eval_cost.startup;
context->total.per_tuple += rinfo->eval_cost.per_tuple;
+ if (rinfo->eval_cost.depth > context->total.depth)
+ context->total.depth = rinfo->eval_cost.depth;
/* do NOT recurse into children */
return false;
}
***************
*** 2694,2699 **** cost_qual_eval_walker(Node *node, cost_qual_eval_context *context)
--- 2699,2706 ----
{
context->total.per_tuple +=
get_func_cost(((FuncExpr *) node)->funcid) * cpu_operator_cost;
+ if (((FuncExpr *)node)->depth > context->total.depth)
+ context->total.depth = ((FuncExpr *)node)->depth;
}
else if (IsA(node, OpExpr) ||
IsA(node, DistinctExpr) ||
***************
*** 2703,2708 **** cost_qual_eval_walker(Node *node, cost_qual_eval_context *context)
--- 2710,2717 ----
set_opfuncid((OpExpr *) node);
context->total.per_tuple +=
get_func_cost(((OpExpr *) node)->opfuncid) * cpu_operator_cost;
+ if (((OpExpr *)node)->depth > context->total.depth)
+ context->total.depth = ((OpExpr *)node)->depth;
}
else if (IsA(node, ScalarArrayOpExpr))
{
***************
*** 2716,2721 **** cost_qual_eval_walker(Node *node, cost_qual_eval_context *context)
--- 2725,2732 ----
set_sa_opfuncid(saop);
context->total.per_tuple += get_func_cost(saop->opfuncid) *
cpu_operator_cost * estimate_array_length(arraynode) * 0.5;
+ if (saop->depth > context->total.depth)
+ context->total.depth = saop->depth;
}
else if (IsA(node, Aggref) ||
IsA(node, WindowFunc))
***************
*** 2746,2751 **** cost_qual_eval_walker(Node *node, cost_qual_eval_context *context)
--- 2757,2764 ----
getTypeOutputInfo(exprType((Node *) iocoerce->arg),
&iofunc, &typisvarlena);
context->total.per_tuple += get_func_cost(iofunc) * cpu_operator_cost;
+ if (iocoerce->depth > context->total.depth)
+ context->total.depth = iocoerce->depth;
}
else if (IsA(node, ArrayCoerceExpr))
{
***************
*** 2755,2760 **** cost_qual_eval_walker(Node *node, cost_qual_eval_context *context)
--- 2768,2775 ----
if (OidIsValid(acoerce->elemfuncid))
context->total.per_tuple += get_func_cost(acoerce->elemfuncid) *
cpu_operator_cost * estimate_array_length(arraynode);
+ if (acoerce->depth > context->total.depth)
+ context->total.depth = acoerce->depth;
}
else if (IsA(node, RowCompareExpr))
{
***************
*** 2769,2774 **** cost_qual_eval_walker(Node *node, cost_qual_eval_context *context)
--- 2784,2791 ----
context->total.per_tuple += get_func_cost(get_opcode(opid)) *
cpu_operator_cost;
}
+ if (rcexpr->depth > context->total.depth)
+ context->total.depth = rcexpr->depth;
}
else if (IsA(node, CurrentOfExpr))
{
*** a/src/backend/optimizer/plan/createplan.c
--- b/src/backend/optimizer/plan/createplan.c
***************
*** 2704,2709 **** order_qual_clauses(PlannerInfo *root, List *clauses)
--- 2704,2710 ----
{
Node *clause;
Cost cost;
+ int depth;
} QualItem;
int nitems = list_length(clauses);
QualItem *items;
***************
*** 2729,2734 **** order_qual_clauses(PlannerInfo *root, List *clauses)
--- 2730,2736 ----
cost_qual_eval_node(&qcost, clause, root);
items[i].clause = clause;
items[i].cost = qcost.per_tuple;
+ items[i].depth = qcost.depth;
i++;
}
***************
*** 2745,2751 **** order_qual_clauses(PlannerInfo *root, List *clauses)
/* insert newitem into the already-sorted subarray */
for (j = i; j > 0; j--)
{
! if (newitem.cost >= items[j - 1].cost)
break;
items[j] = items[j - 1];
}
--- 2747,2762 ----
/* insert newitem into the already-sorted subarray */
for (j = i; j > 0; j--)
{
! /*
! * Higher priority shall be given to the items originated from
! * deeper nest level. If same level, it shall be given to the
! * items with smaller estimated cost.
! * Such kind of consideration is needed to prevent leaky-view
! * problem.
! */
! if (newitem.depth < items[j - 1].depth ||
! (newitem.depth == items[j - 1].depth &&
! newitem.cost >= items[j - 1].cost))
break;
items[j] = items[j - 1];
}
*** a/src/backend/optimizer/plan/planner.c
--- b/src/backend/optimizer/plan/planner.c
***************
*** 33,38 ****
--- 33,39 ----
#include "optimizer/subselect.h"
#include "optimizer/tlist.h"
#include "optimizer/var.h"
+ #include "nodes/nodeFuncs.h"
#ifdef OPTIMIZER_DEBUG
#include "nodes/print.h"
#endif
***************
*** 104,109 **** static void get_column_info_for_window(PlannerInfo *root, WindowClause *wc,
--- 105,111 ----
int *ordNumCols,
AttrNumber **ordColIdx,
Oid **ordOperators);
+ static void mark_qualifiers_depth(Query *query, int depth);
/*****************************************************************************
***************
*** 149,154 **** standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams)
--- 151,162 ----
cursorOptions |= ((DeclareCursorStmt *) parse->utilityStmt)->options;
/*
+ * Mark qualifiers its original depth to prevent reversal of orders
+ * on evaluation of WHERE clause during relation scanns.
+ */
+ mark_qualifiers_depth(parse, 0);
+
+ /*
* Set up global state for this planner invocation. This data is needed
* across all levels of sub-Query that might exist in the given command,
* so we keep it in a separate struct that's linked to by each per-Query
***************
*** 2994,2999 **** get_column_info_for_window(PlannerInfo *root, WindowClause *wc, List *tlist,
--- 3002,3127 ----
}
}
+ /*
+ * mark_qualifiers_depth
+ *
+ * It marks depth field of the each expression nodes that eventually
+ * invokes functions, to track the original nest-level. On the evaluation
+ * of qualifiers within WHERE or JOIN ... ON clauses during relation scans,
+ * these items shall be reordered according to the nest-level and estimated
+ * cost.
+ * The optimizer may pull-up simple sub-queries or join clause, and
+ * qualifiers to filter out tuples shall be mixed with ones in upper-
+ * level. Thus, we need to track the original nest-level of qualifiers
+ * to prevent reverse of order in evaluation, because some of qualifiers
+ * can have side-effects that allows to leak supplied argument to outside.
+ * It can be abused to break row-level security using a user defined function
+ * with very small estimated cost, so nest level of qualifiers originated
+ * from is used as a criteria, rather than estimated cost, to decide order
+ * to evaluate qualifiers.
+ */
+ static bool
+ mark_qualifiers_depth_walker(Node *node, void *context)
+ {
+ int depth = *((int *)(context));
+
+ if (node == NULL)
+ return false;
+ if (IsA(node, FuncExpr))
+ {
+ FuncExpr *exp = (FuncExpr *)node;
+
+ exp->depth = depth;
+
+ return false;
+ }
+ else if (IsA(node, OpExpr))
+ {
+ OpExpr *exp = (OpExpr *)node;
+
+ exp->depth = depth;
+
+ return false;
+ }
+ else if (IsA(node, DistinctExpr))
+ {
+ DistinctExpr *exp = (DistinctExpr *)node;
+
+ exp->depth = depth;
+
+ return false;
+ }
+ else if (IsA(node, ScalarArrayOpExpr))
+ {
+ ScalarArrayOpExpr *exp = (ScalarArrayOpExpr *)node;
+
+ exp->depth = depth;
+
+ return false;
+ }
+ else if (IsA(node, CoerceViaIO))
+ {
+ CoerceViaIO *exp = (CoerceViaIO *)node;
+
+ exp->depth = depth;
+
+ return false;
+ }
+ else if (IsA(node, ArrayCoerceExpr))
+ {
+ ArrayCoerceExpr *exp = (ArrayCoerceExpr *)node;
+
+ exp->depth = depth;
+
+ return false;
+ }
+ else if (IsA(node, NullIfExpr))
+ {
+ NullIfExpr *exp = (NullIfExpr *)node;
+
+ exp->depth = depth;
+
+ return false;
+ }
+ else if (IsA(node, RowCompareExpr))
+ {
+ RowCompareExpr *exp = (RowCompareExpr *)node;
+
+ exp->depth = depth;
+
+ return false;
+ }
+ return expression_tree_walker(node, mark_qualifiers_depth_walker, context);
+ }
+
+ static void
+ mark_qualifiers_depth(Query *query, int depth)
+ {
+ ListCell *l;
+
+ foreach (l, query->rtable)
+ {
+ RangeTblEntry *rte = lfirst(l);
+
+ /*
+ * If and when sub-query is defined as a security-barrier,
+ * any qualifiers of WHERE or JOIN ... ON clause must be
+ * launched earlier than ones come from upper nest level,
+ * even if the sub-query is enough simple to be pulled-up
+ * later, because user can reference contents of tuples to
+ * be invisible using functions with side-effect and much
+ * smaller cost estimation.
+ */
+ if (rte->rtekind == RTE_SUBQUERY)
+ {
+ if (rte->security_barrier)
+ mark_qualifiers_depth(rte->subquery, depth + 1);
+ else
+ mark_qualifiers_depth(rte->subquery, depth);
+ }
+ }
+ mark_qualifiers_depth_walker((Node *)query->jointree, &depth);
+ }
/*
* expression_planner
*** a/src/backend/optimizer/util/clauses.c
--- b/src/backend/optimizer/util/clauses.c
***************
*** 111,116 **** static Expr *simplify_function(Expr *oldexpr, Oid funcid,
--- 111,117 ----
Oid input_collid, List **args,
bool has_named_args,
bool allow_inline,
+ int depth,
eval_const_expressions_context *context);
static List *reorder_function_arguments(List *args, Oid result_type,
HeapTuple func_tuple,
***************
*** 123,129 **** static void recheck_cast_function_args(List *args, Oid result_type,
HeapTuple func_tuple);
static Expr *evaluate_function(Oid funcid, Oid result_type, int32 result_typmod,
Oid result_collid, Oid input_collid, List *args,
! HeapTuple func_tuple,
eval_const_expressions_context *context);
static Expr *inline_function(Oid funcid, Oid result_type, Oid result_collid,
Oid input_collid, List *args,
--- 124,130 ----
HeapTuple func_tuple);
static Expr *evaluate_function(Oid funcid, Oid result_type, int32 result_typmod,
Oid result_collid, Oid input_collid, List *args,
! HeapTuple func_tuple, int depth,
eval_const_expressions_context *context);
static Expr *inline_function(Oid funcid, Oid result_type, Oid result_collid,
Oid input_collid, List *args,
***************
*** 2229,2235 **** eval_const_expressions_mutator(Node *node,
expr->funccollid,
expr->inputcollid,
&args,
! has_named_args, true, context);
if (simple) /* successfully simplified it */
return (Node *) simple;
--- 2230,2236 ----
expr->funccollid,
expr->inputcollid,
&args,
! has_named_args, true, expr->depth, context);
if (simple) /* successfully simplified it */
return (Node *) simple;
***************
*** 2248,2253 **** eval_const_expressions_mutator(Node *node,
--- 2249,2255 ----
newexpr->inputcollid = expr->inputcollid;
newexpr->args = args;
newexpr->location = expr->location;
+ newexpr->depth = expr->depth;
return (Node *) newexpr;
}
if (IsA(node, OpExpr))
***************
*** 2282,2288 **** eval_const_expressions_mutator(Node *node,
expr->opcollid,
expr->inputcollid,
&args,
! false, true, context);
if (simple) /* successfully simplified it */
return (Node *) simple;
--- 2284,2290 ----
expr->opcollid,
expr->inputcollid,
&args,
! false, true, expr->depth, context);
if (simple) /* successfully simplified it */
return (Node *) simple;
***************
*** 2313,2318 **** eval_const_expressions_mutator(Node *node,
--- 2315,2321 ----
newexpr->inputcollid = expr->inputcollid;
newexpr->args = args;
newexpr->location = expr->location;
+ newexpr->depth = expr->depth;
return (Node *) newexpr;
}
if (IsA(node, DistinctExpr))
***************
*** 2380,2386 **** eval_const_expressions_mutator(Node *node,
expr->opcollid,
expr->inputcollid,
&args,
! false, false, context);
if (simple) /* successfully simplified it */
{
/*
--- 2383,2389 ----
expr->opcollid,
expr->inputcollid,
&args,
! false, false, expr->depth, context);
if (simple) /* successfully simplified it */
{
/*
***************
*** 2410,2415 **** eval_const_expressions_mutator(Node *node,
--- 2413,2419 ----
newexpr->inputcollid = expr->inputcollid;
newexpr->args = args;
newexpr->location = expr->location;
+ newexpr->depth = expr->depth;
return (Node *) newexpr;
}
if (IsA(node, BoolExpr))
***************
*** 2570,2576 **** eval_const_expressions_mutator(Node *node,
InvalidOid,
InvalidOid,
&args,
! false, true, context);
if (simple) /* successfully simplified output fn */
{
/*
--- 2574,2580 ----
InvalidOid,
InvalidOid,
&args,
! false, true, expr->depth, context);
if (simple) /* successfully simplified output fn */
{
/*
***************
*** 2591,2597 **** eval_const_expressions_mutator(Node *node,
expr->resultcollid,
InvalidOid,
&args,
! false, true, context);
if (simple) /* successfully simplified input fn */
return (Node *) simple;
}
--- 2595,2601 ----
expr->resultcollid,
InvalidOid,
&args,
! false, true, expr->depth, context);
if (simple) /* successfully simplified input fn */
return (Node *) simple;
}
***************
*** 2607,2612 **** eval_const_expressions_mutator(Node *node,
--- 2611,2617 ----
newexpr->resultcollid = expr->resultcollid;
newexpr->coerceformat = expr->coerceformat;
newexpr->location = expr->location;
+ newexpr->depth = expr->depth;
return (Node *) newexpr;
}
if (IsA(node, ArrayCoerceExpr))
***************
*** 2631,2636 **** eval_const_expressions_mutator(Node *node,
--- 2636,2642 ----
newexpr->isExplicit = expr->isExplicit;
newexpr->coerceformat = expr->coerceformat;
newexpr->location = expr->location;
+ newexpr->depth = expr->depth;
/*
* If constant argument and it's a binary-coercible or immutable
***************
*** 3447,3452 **** simplify_function(Expr *oldexpr, Oid funcid,
--- 3453,3459 ----
Oid input_collid, List **args,
bool has_named_args,
bool allow_inline,
+ int depth,
eval_const_expressions_context *context)
{
HeapTuple func_tuple;
***************
*** 3477,3483 **** simplify_function(Expr *oldexpr, Oid funcid,
newexpr = evaluate_function(funcid, result_type, result_typmod,
result_collid, input_collid, *args,
! func_tuple, context);
/*
* Some functions calls can be simplified at plan time based on properties
--- 3484,3490 ----
newexpr = evaluate_function(funcid, result_type, result_typmod,
result_collid, input_collid, *args,
! func_tuple, depth, context);
/*
* Some functions calls can be simplified at plan time based on properties
***************
*** 3764,3770 **** recheck_cast_function_args(List *args, Oid result_type, HeapTuple func_tuple)
static Expr *
evaluate_function(Oid funcid, Oid result_type, int32 result_typmod,
Oid result_collid, Oid input_collid, List *args,
! HeapTuple func_tuple,
eval_const_expressions_context *context)
{
Form_pg_proc funcform = (Form_pg_proc) GETSTRUCT(func_tuple);
--- 3771,3777 ----
static Expr *
evaluate_function(Oid funcid, Oid result_type, int32 result_typmod,
Oid result_collid, Oid input_collid, List *args,
! HeapTuple func_tuple, int depth,
eval_const_expressions_context *context)
{
Form_pg_proc funcform = (Form_pg_proc) GETSTRUCT(func_tuple);
***************
*** 3850,3855 **** evaluate_function(Oid funcid, Oid result_type, int32 result_typmod,
--- 3857,3863 ----
newexpr->inputcollid = input_collid;
newexpr->args = args;
newexpr->location = -1;
+ newexpr->depth = depth;
return evaluate_expr((Expr *) newexpr, result_type, result_typmod,
result_collid);
*** a/src/include/nodes/primnodes.h
--- b/src/include/nodes/primnodes.h
***************
*** 335,340 **** typedef struct FuncExpr
--- 335,341 ----
Oid inputcollid; /* OID of collation that function should use */
List *args; /* arguments to the function */
int location; /* token location, or -1 if unknown */
+ int depth; /* depth of clause in the original query */
} FuncExpr;
/*
***************
*** 380,385 **** typedef struct OpExpr
--- 381,387 ----
Oid inputcollid; /* OID of collation that operator should use */
List *args; /* arguments to the operator (1 or 2) */
int location; /* token location, or -1 if unknown */
+ int depth; /* depth of clause in the original query */
} OpExpr;
/*
***************
*** 421,426 **** typedef struct ScalarArrayOpExpr
--- 423,429 ----
Oid inputcollid; /* OID of collation that operator should use */
List *args; /* the scalar and array operands */
int location; /* token location, or -1 if unknown */
+ int depth; /* depth of clause in the original query */
} ScalarArrayOpExpr;
/*
***************
*** 685,690 **** typedef struct CoerceViaIO
--- 688,694 ----
Oid resultcollid; /* OID of collation, or InvalidOid if none */
CoercionForm coerceformat; /* how to display this node */
int location; /* token location, or -1 if unknown */
+ int depth; /* depth of clause in the original query */
} CoerceViaIO;
/* ----------------
***************
*** 710,715 **** typedef struct ArrayCoerceExpr
--- 714,720 ----
bool isExplicit; /* conversion semantics flag to pass to func */
CoercionForm coerceformat; /* how to display this node */
int location; /* token location, or -1 if unknown */
+ int depth; /* depth of clause in the original query */
} ArrayCoerceExpr;
/* ----------------
***************
*** 901,906 **** typedef struct RowCompareExpr
--- 906,912 ----
List *inputcollids; /* OID list of collations for comparisons */
List *largs; /* the left-hand input arguments */
List *rargs; /* the right-hand input arguments */
+ int depth; /* depth of clause in the original query */
} RowCompareExpr;
/*
*** a/src/include/nodes/relation.h
--- b/src/include/nodes/relation.h
***************
*** 44,49 **** typedef struct QualCost
--- 44,50 ----
{
Cost startup; /* one-time cost */
Cost per_tuple; /* per-evaluation cost */
+ int depth; /* depth of qual in the original query */
} QualCost;
/*
*** a/src/test/regress/expected/select_views.out
--- b/src/test/regress/expected/select_views.out
***************
*** 467,472 **** SELECT name, #thepath FROM iexit ORDER BY 1, 2;
--- 467,486 ----
I- 580 | 21
I- 580 | 22
I- 580 | 22
+ I- 580/I-680 Ramp | 2
+ I- 580/I-680 Ramp | 2
+ I- 580/I-680 Ramp | 2
+ I- 580/I-680 Ramp | 2
+ I- 580/I-680 Ramp | 2
+ I- 580/I-680 Ramp | 2
+ I- 580/I-680 Ramp | 4
+ I- 580/I-680 Ramp | 4
+ I- 580/I-680 Ramp | 4
+ I- 580/I-680 Ramp | 4
+ I- 580/I-680 Ramp | 5
+ I- 580/I-680 Ramp | 6
+ I- 580/I-680 Ramp | 6
+ I- 580/I-680 Ramp | 6
I- 580 Ramp | 2
I- 580 Ramp | 2
I- 580 Ramp | 2
***************
*** 717,736 **** SELECT name, #thepath FROM iexit ORDER BY 1, 2;
I- 580 Ramp | 8
I- 580 Ramp | 8
I- 580 Ramp | 8
- I- 580/I-680 Ramp | 2
- I- 580/I-680 Ramp | 2
- I- 580/I-680 Ramp | 2
- I- 580/I-680 Ramp | 2
- I- 580/I-680 Ramp | 2
- I- 580/I-680 Ramp | 2
- I- 580/I-680 Ramp | 4
- I- 580/I-680 Ramp | 4
- I- 580/I-680 Ramp | 4
- I- 580/I-680 Ramp | 4
- I- 580/I-680 Ramp | 5
- I- 580/I-680 Ramp | 6
- I- 580/I-680 Ramp | 6
- I- 580/I-680 Ramp | 6
I- 680 | 2
I- 680 | 2
I- 680 | 2
--- 731,736 ----
***************
*** 1247,1249 **** SELECT * FROM toyemp WHERE name = 'sharon';
--- 1247,1312 ----
sharon | 25 | (15,12) | 12000
(1 row)
+ --
+ -- Test for leaky-view
+ --
+ CREATE USER alice;
+ CREATE FUNCTION f_leak(text, text)
+ RETURNS bool LANGUAGE 'plpgsql'
+ COST 0.00000001
+ AS 'begin raise notice ''% => %'', $1, $2; return true; end';
+ CREATE TABLE credit_cards (
+ name text,
+ number text,
+ expired text
+ );
+ INSERT INTO credit_cards VALUES ('alice', '1111-2222-3333-4444', 'Aug-2012'),
+ ('bob', '5555-6666-7777-8888', 'Nov-2016'),
+ ('eve', '9801-2345-6789-0123', 'Jan-2018');
+ CREATE VIEW your_credit_normal AS
+ SELECT * FROM credit_cards WHERE name = getpgusername();
+ CREATE VIEW your_credit_secure WITH (security_barrier) AS
+ SELECT * FROM credit_cards WHERE name = getpgusername();
+ GRANT SELECT ON your_credit_normal TO public;
+ GRANT SELECT ON your_credit_secure TO public;
+ -- run leaky view
+ SET SESSION AUTHORIZATION alice;
+ SELECT * FROM your_credit_normal WHERE f_leak(number,expired);
+ NOTICE: 1111-2222-3333-4444 => Aug-2012
+ NOTICE: 5555-6666-7777-8888 => Nov-2016
+ NOTICE: 9801-2345-6789-0123 => Jan-2018
+ name | number | expired
+ -------+---------------------+----------
+ alice | 1111-2222-3333-4444 | Aug-2012
+ (1 row)
+
+ EXPLAIN (COSTS OFF) SELECT * FROM your_credit_normal WHERE f_leak(number,expired);
+ QUERY PLAN
+ --------------------------------------------------------------------------
+ Seq Scan on credit_cards
+ Filter: (f_leak(number, expired) AND (name = (getpgusername())::text))
+ (2 rows)
+
+ SELECT * FROM your_credit_secure WHERE f_leak(number,expired);
+ NOTICE: 1111-2222-3333-4444 => Aug-2012
+ name | number | expired
+ -------+---------------------+----------
+ alice | 1111-2222-3333-4444 | Aug-2012
+ (1 row)
+
+ EXPLAIN (COSTS OFF) SELECT * FROM your_credit_secure WHERE f_leak(number,expired);
+ QUERY PLAN
+ --------------------------------------------------------------------------
+ Seq Scan on credit_cards
+ Filter: ((name = (getpgusername())::text) AND f_leak(number, expired))
+ (2 rows)
+
+ \c -
+ -- cleanups
+ DROP ROLE IF EXISTS alice;
+ DROP FUNCTION IF EXISTS f_leak(text);
+ NOTICE: function f_leak(text) does not exist, skipping
+ DROP TABLE IF EXISTS credit_cards CASCADE;
+ NOTICE: drop cascades to 2 other objects
+ DETAIL: drop cascades to view your_credit_normal
+ drop cascades to view your_credit_secure
*** a/src/test/regress/sql/select_views.sql
--- b/src/test/regress/sql/select_views.sql
***************
*** 8,10 **** SELECT * FROM street;
--- 8,49 ----
SELECT name, #thepath FROM iexit ORDER BY 1, 2;
SELECT * FROM toyemp WHERE name = 'sharon';
+
+ --
+ -- Test for leaky-view
+ --
+
+ CREATE USER alice;
+ CREATE FUNCTION f_leak(text, text)
+ RETURNS bool LANGUAGE 'plpgsql'
+ COST 0.00000001
+ AS 'begin raise notice ''% => %'', $1, $2; return true; end';
+ CREATE TABLE credit_cards (
+ name text,
+ number text,
+ expired text
+ );
+ INSERT INTO credit_cards VALUES ('alice', '1111-2222-3333-4444', 'Aug-2012'),
+ ('bob', '5555-6666-7777-8888', 'Nov-2016'),
+ ('eve', '9801-2345-6789-0123', 'Jan-2018');
+ CREATE VIEW your_credit_normal AS
+ SELECT * FROM credit_cards WHERE name = getpgusername();
+ CREATE VIEW your_credit_secure WITH (security_barrier) AS
+ SELECT * FROM credit_cards WHERE name = getpgusername();
+
+ GRANT SELECT ON your_credit_normal TO public;
+ GRANT SELECT ON your_credit_secure TO public;
+ -- run leaky view
+ SET SESSION AUTHORIZATION alice;
+
+ SELECT * FROM your_credit_normal WHERE f_leak(number,expired);
+ EXPLAIN (COSTS OFF) SELECT * FROM your_credit_normal WHERE f_leak(number,expired);
+
+ SELECT * FROM your_credit_secure WHERE f_leak(number,expired);
+ EXPLAIN (COSTS OFF) SELECT * FROM your_credit_secure WHERE f_leak(number,expired);
+
+ \c -
+ -- cleanups
+ DROP ROLE IF EXISTS alice;
+ DROP FUNCTION IF EXISTS f_leak(text);
+ DROP TABLE IF EXISTS credit_cards CASCADE;
On Wed, Jul 06, 2011 at 10:25:12PM +0200, Kohei KaiGai wrote:
*** a/src/backend/commands/view.c --- b/src/backend/commands/view.c
--- 227,257 ---- atcmd->def = (Node *) lfirst(c); atcmds = lappend(atcmds, atcmd); } }/* + * If optional parameters are specified, we must set options + * using ALTER TABLE SET OPTION internally. + */ + if (list_length(options) > 0) + { + atcmd = makeNode(AlterTableCmd); + atcmd->subtype = AT_SetRelOptions; + atcmd->def = (List *)options; + + atcmds = lappend(atcmds, atcmd); + } + else + { + atcmd = makeNode(AlterTableCmd); + atcmd->subtype = AT_ResetRelOptions; + atcmd->def = (Node *) list_make1(makeDefElem("security_barrier", + NULL)); + } + if (atcmds != NIL) + AlterTableInternal(viewOid, atcmds, true); + + /* * Seems okay, so return the OID of the pre-existing view. */ relation_close(rel, NoLock); /* keep the lock! */
That gets the job done for today, but DefineVirtualRelation() should not need
to know all view options by name to simply replace the existing list with a
new one. I don't think you can cleanly use the ALTER TABLE SET/RESET code for
this. Instead, compute an option list similar to how DefineRelation() does so
at tablecmds.c:491, then update pg_class.
2011/7/5 Noah Misch <noah@2ndquadrant.com>:
On Sun, Jul 03, 2011 at 11:33:38AM +0200, Kohei KaiGai wrote:
--- a/src/test/regress/sql/select_views.sql +++ b/src/test/regress/sql/select_views.sql
+-- cleanups +DROP ROLE IF EXISTS alice; +DROP FUNCTION IF EXISTS f_leak(text); +DROP TABLE IF EXISTS credit_cards CASCADE;Keep the view around. �That way, pg_dump tests of the regression database will
test the dumping of this view option. �(Your pg_dump support for this feature
does work fine, though.)
The latest version of part 1 still drops everything here.
2011/7/7 Noah Misch <noah@2ndquadrant.com>:
On Wed, Jul 06, 2011 at 10:25:12PM +0200, Kohei KaiGai wrote:
*** a/src/backend/commands/view.c --- b/src/backend/commands/view.c--- 227,257 ---- atcmd->def = (Node *) lfirst(c); atcmds = lappend(atcmds, atcmd); } }/* + * If optional parameters are specified, we must set options + * using ALTER TABLE SET OPTION internally. + */ + if (list_length(options) > 0) + { + atcmd = makeNode(AlterTableCmd); + atcmd->subtype = AT_SetRelOptions; + atcmd->def = (List *)options; + + atcmds = lappend(atcmds, atcmd); + } + else + { + atcmd = makeNode(AlterTableCmd); + atcmd->subtype = AT_ResetRelOptions; + atcmd->def = (Node *) list_make1(makeDefElem("security_barrier", + NULL)); + } + if (atcmds != NIL) + AlterTableInternal(viewOid, atcmds, true); + + /* * Seems okay, so return the OID of the pre-existing view. */ relation_close(rel, NoLock); /* keep the lock! */That gets the job done for today, but DefineVirtualRelation() should not need
to know all view options by name to simply replace the existing list with a
new one. I don't think you can cleanly use the ALTER TABLE SET/RESET code for
this. Instead, compute an option list similar to how DefineRelation() does so
at tablecmds.c:491, then update pg_class.
My opinion is ALTER TABLE SET/RESET code should be enhanced to accept
an operation to reset all the existing options, rather than tricky
updates of pg_class.
How about an idea to add AT_ResetAllRelOptions for internal use only?
I'll fix up the regression test also.
Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
On Thu, Jul 7, 2011 at 10:56 AM, Kohei KaiGai <kaigai@kaigai.gr.jp> wrote:
My opinion is ALTER TABLE SET/RESET code should be enhanced to accept
an operation to reset all the existing options, rather than tricky
updates of pg_class.
How about an idea to add AT_ResetAllRelOptions for internal use only?I'll fix up the regression test also.
On an only semi-related note, ISTM that you may as well merge parts 0,
1, and 2 into a single patch, since there is no way we are going to
apply any of them without the others. I suggest closing one of the
CommitFest entries and revising the other one to point to the
consolidated patch.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
On Thu, Jul 07, 2011 at 03:56:26PM +0100, Kohei KaiGai wrote:
2011/7/7 Noah Misch <noah@2ndquadrant.com>:
On Wed, Jul 06, 2011 at 10:25:12PM +0200, Kohei KaiGai wrote:
*** a/src/backend/commands/view.c --- b/src/backend/commands/view.c--- 227,257 ---- � � � � � � � � � � � � � � � atcmd->def = (Node *) lfirst(c); � � � � � � � � � � � � � � � atcmds = lappend(atcmds, atcmd); � � � � � � � � � � � } � � � � � � � }� � � � � � � /* + � � � � � � �* If optional parameters are specified, we must set options + � � � � � � �* using ALTER TABLE SET OPTION internally. + � � � � � � �*/ + � � � � � � if (list_length(options) > 0) + � � � � � � { + � � � � � � � � � � atcmd = makeNode(AlterTableCmd); + � � � � � � � � � � atcmd->subtype = AT_SetRelOptions; + � � � � � � � � � � atcmd->def = (List *)options; + + � � � � � � � � � � atcmds = lappend(atcmds, atcmd); + � � � � � � } + � � � � � � else + � � � � � � { + � � � � � � � � � � atcmd = makeNode(AlterTableCmd); + � � � � � � � � � � atcmd->subtype = AT_ResetRelOptions; + � � � � � � � � � � atcmd->def = (Node *) list_make1(makeDefElem("security_barrier", + � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � �NULL)); + � � � � � � } + � � � � � � if (atcmds != NIL) + � � � � � � � � � � AlterTableInternal(viewOid, atcmds, true); + + � � � � � � /* � � � � � � � �* Seems okay, so return the OID of the pre-existing view. � � � � � � � �*/ � � � � � � � relation_close(rel, NoLock); � �/* keep the lock! */That gets the job done for today, but DefineVirtualRelation() should not need
to know all view options by name to simply replace the existing list with a
new one. �I don't think you can cleanly use the ALTER TABLE SET/RESET code for
this. �Instead, compute an option list similar to how DefineRelation() does so
at tablecmds.c:491, then update pg_class.My opinion is ALTER TABLE SET/RESET code should be enhanced to accept
an operation to reset all the existing options, rather than tricky
updates of pg_class.
The pg_class update has ~20 lines of idiomatic code; see tablecmds.c:7931-7951.
How about an idea to add AT_ResetAllRelOptions for internal use only?
If some operation is purely internal and does not otherwise benefit from the
ALTER TABLE infrastructure, there's no benefit in involving ALTER TABLE.
DefineVirtualRelation() uses ALTER TABLE to add columns because all that code
needs to exist anyway. You could make a plain function to do the update that
gets called from both ATExecSetRelOptions() and DefineVirtualRelation().
Thanks,
nm
On Sun, Jul 03, 2011 at 11:41:47AM +0200, Kohei KaiGai wrote:
The simplified version of fix-leaky-view patch. The part of reloptions
for views got splitted out
into the part-0 patch, so it needs to be applied prior to this patch.
Rest of logic to prevent unexpected pushing down across security
barrier is not changed.Thanks,
2011/6/6 Kohei Kaigai <Kohei.Kaigai@emea.nec.com>:
This patch enables to fix up leaky-view problem using qualifiers that reference only one-side of join-loop inside of view definition.
The point of this scenario is criteria to distribute qualifiers of scanning-plan distributed in distribute_qual_to_rels(). If and when a qualifiers that reference only one-side of join-loop, the optimizer may distribute this qualifier into inside of the join-loop, even if it goes over the boundary of a subquery expanded from a view for row-level security.
This behavior allows us to reference whole of one-side of join-loop using functions with side-effects.
The solution is quite simple; it prohibits to distribute qualifiers over the boundary of subquery, however, performance cost is unignorable, because it also disables to utilize obviously indexable qualifiers such as (id=123), so this patch requires users a hint whether a particular view is for row-level security, or not.This patch newly adds "CREATE SECURITY VIEW" statement that marks a flag to show this view was defined for row-level security purpose. This flag shall be stored as reloptions.
If this flag was set, the optimizer does not distribute qualifiers over the boundary of subqueries expanded from security views, except for obviously safe qualifiers.
(Right now, we consider built-in indexable operators are safe, but it might be arguable.)
I took a moderately-detailed look at this patch. This jumped out:
--- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c
+static bool +contain_leakable_functions_walker(Node *node, void *context) +{ + if (node == NULL) + return false; + + if (IsA(node, FuncExpr)) + { + /* + * Right now, we have no way to distinguish safe functions with + * leakable ones, so, we treat all the function call possibly + * leakable. + */ + return true; + } + else if (IsA(node, OpExpr)) + { + OpExpr *expr = (OpExpr *) node; + + /* + * Right now, we assume operators implemented by built-in functions + * are not leakable, so it does not need to prevent optimization. + */ + set_opfuncid(expr); + if (get_func_lang(expr->opfuncid) != INTERNALlanguageId) + return true; + /* else fall through to check args */ + }
Any user can do this:
CREATE OPERATOR !-! (PROCEDURE = int4in, RIGHTARG = cstring);
SELECT !-! 'foo';
Making a distinction based simply on the call being an operator vs. a function
is a dead end. I see these options:
1. The user defining a security view can be assumed to trust the operator class
members of indexes defined on the tables he references. Keep track of which
those are and treat only them as non-leakable. This covers many interesting
cases, but it's probably tricky to implement and/or costly at runtime.
2. Add a pg_proc flag indicating whether the function is known leak-free.
Simple, but tedious and perhaps error-prone.
3. Trust operators owned by PGUID. This is simple and probably covers the
essential cases, but it's an ugly hack.
4. Trust nothing as leak-free. Simple; performance will be unattractive.
There are probably others.
2011/7/7 Noah Misch <noah@2ndquadrant.com>:
On Sun, Jul 03, 2011 at 11:41:47AM +0200, Kohei KaiGai wrote:
The simplified version of fix-leaky-view patch. The part of reloptions
for views got splitted out
into the part-0 patch, so it needs to be applied prior to this patch.
Rest of logic to prevent unexpected pushing down across security
barrier is not changed.Thanks,
2011/6/6 Kohei Kaigai <Kohei.Kaigai@emea.nec.com>:
This patch enables to fix up leaky-view problem using qualifiers that reference only one-side of join-loop inside of view definition.
The point of this scenario is criteria to distribute qualifiers of scanning-plan distributed in distribute_qual_to_rels(). If and when a qualifiers that reference only one-side of join-loop, the optimizer may distribute this qualifier into inside of the join-loop, even if it goes over the boundary of a subquery expanded from a view for row-level security.
This behavior allows us to reference whole of one-side of join-loop using functions with side-effects.
The solution is quite simple; it prohibits to distribute qualifiers over the boundary of subquery, however, performance cost is unignorable, because it also disables to utilize obviously indexable qualifiers such as (id=123), so this patch requires users a hint whether a particular view is for row-level security, or not.This patch newly adds "CREATE SECURITY VIEW" statement that marks a flag to show this view was defined for row-level security purpose. This flag shall be stored as reloptions.
If this flag was set, the optimizer does not distribute qualifiers over the boundary of subqueries expanded from security views, except for obviously safe qualifiers.
(Right now, we consider built-in indexable operators are safe, but it might be arguable.)I took a moderately-detailed look at this patch. This jumped out:
--- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c+static bool +contain_leakable_functions_walker(Node *node, void *context) +{ + if (node == NULL) + return false; + + if (IsA(node, FuncExpr)) + { + /* + * Right now, we have no way to distinguish safe functions with + * leakable ones, so, we treat all the function call possibly + * leakable. + */ + return true; + } + else if (IsA(node, OpExpr)) + { + OpExpr *expr = (OpExpr *) node; + + /* + * Right now, we assume operators implemented by built-in functions + * are not leakable, so it does not need to prevent optimization. + */ + set_opfuncid(expr); + if (get_func_lang(expr->opfuncid) != INTERNALlanguageId) + return true; + /* else fall through to check args */ + }Any user can do this:
CREATE OPERATOR !-! (PROCEDURE = int4in, RIGHTARG = cstring);
SELECT !-! 'foo';
As I mentioned at the source code comments, this ad-hoc assumption was
come from we have no way to distinguish a non-leaky function from others.
So, I definitely love the approach (2), because only trusted function creator
can determine whether it is possible leaky or not.
Making a distinction based simply on the call being an operator vs. a function
is a dead end. I see these options:1. The user defining a security view can be assumed to trust the operator class
members of indexes defined on the tables he references. Keep track of which
those are and treat only them as non-leakable. This covers many interesting
cases, but it's probably tricky to implement and/or costly at runtime.
It requires DBA massive amount of detailed knowledge about functions underlying
operators used in a view. I don't think it is a realistic assumption.
2. Add a pg_proc flag indicating whether the function is known leak-free.
Simple, but tedious and perhaps error-prone.
+1
3. Trust operators owned by PGUID. This is simple and probably covers the
essential cases, but it's an ugly hack.
Some of built-in functions are also leaky. For example, int4div raise an error
when we try to divid a particular value by zero.
4. Trust nothing as leak-free. Simple; performance will be unattractive.
-1, Because of performance perspective.
Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
On 08.07.2011 11:03, Kohei KaiGai wrote:
2011/7/7 Noah Misch<noah@2ndquadrant.com>:
Making a distinction based simply on the call being an operator vs. a function
is a dead end. I see these options:1. The user defining a security view can be assumed to trust the operator class
members of indexes defined on the tables he references. Keep track of which
those are and treat only them as non-leakable. This covers many interesting
cases, but it's probably tricky to implement and/or costly at runtime.It requires DBA massive amount of detailed knowledge about functions underlying
operators used in a view. I don't think it is a realistic assumption.2. Add a pg_proc flag indicating whether the function is known leak-free.
Simple, but tedious and perhaps error-prone.+1
IMHO the situation from DBA's point of view is exactly opposite. Option
two requires deep knowledge of this leaky views issue. The DBA needs to
inspect any function he wants to mark as leak-free closely, and
understand that innocent-looking things like casts can cause leaks. That
is not feasible in practice. Option 1, however, requires no such
knowledge. Operators used in indexes are already expected to not throw
errors, or you would get errors when inserting certain values to the
table, for example.
--
Heikki Linnakangas
EnterpriseDB http://www.enterprisedb.com
2011/7/7 Noah Misch <noah@2ndquadrant.com>:
On Thu, Jul 07, 2011 at 03:56:26PM +0100, Kohei KaiGai wrote:
2011/7/7 Noah Misch <noah@2ndquadrant.com>:
On Wed, Jul 06, 2011 at 10:25:12PM +0200, Kohei KaiGai wrote:
*** a/src/backend/commands/view.c --- b/src/backend/commands/view.c--- 227,257 ---- atcmd->def = (Node *) lfirst(c); atcmds = lappend(atcmds, atcmd); } }/* + * If optional parameters are specified, we must set options + * using ALTER TABLE SET OPTION internally. + */ + if (list_length(options) > 0) + { + atcmd = makeNode(AlterTableCmd); + atcmd->subtype = AT_SetRelOptions; + atcmd->def = (List *)options; + + atcmds = lappend(atcmds, atcmd); + } + else + { + atcmd = makeNode(AlterTableCmd); + atcmd->subtype = AT_ResetRelOptions; + atcmd->def = (Node *) list_make1(makeDefElem("security_barrier", + NULL)); + } + if (atcmds != NIL) + AlterTableInternal(viewOid, atcmds, true); + + /* * Seems okay, so return the OID of the pre-existing view. */ relation_close(rel, NoLock); /* keep the lock! */That gets the job done for today, but DefineVirtualRelation() should not need
to know all view options by name to simply replace the existing list with a
new one. I don't think you can cleanly use the ALTER TABLE SET/RESET code for
this. Instead, compute an option list similar to how DefineRelation() does so
at tablecmds.c:491, then update pg_class.My opinion is ALTER TABLE SET/RESET code should be enhanced to accept
an operation to reset all the existing options, rather than tricky
updates of pg_class.The pg_class update has ~20 lines of idiomatic code; see tablecmds.c:7931-7951.
Even if idiomatic, another part of DefineVirtualRelation() uses
AlterTableInternal().
I think a common way is more straightforward.
So, how about an idea to add a function that pull-out existing options
from syscache,
and merge with the supplied options list prior to AlterTableInternal()?
Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
2011/7/8 Heikki Linnakangas <heikki.linnakangas@enterprisedb.com>:
On 08.07.2011 11:03, Kohei KaiGai wrote:
2011/7/7 Noah Misch<noah@2ndquadrant.com>:
Making a distinction based simply on the call being an operator vs. a
function
is a dead end. I see these options:1. The user defining a security view can be assumed to trust the operator
class
members of indexes defined on the tables he references. Keep track of
which
those are and treat only them as non-leakable. This covers many
interesting
cases, but it's probably tricky to implement and/or costly at runtime.It requires DBA massive amount of detailed knowledge about functions
underlying
operators used in a view. I don't think it is a realistic assumption.2. Add a pg_proc flag indicating whether the function is known leak-free.
Simple, but tedious and perhaps error-prone.+1
IMHO the situation from DBA's point of view is exactly opposite. Option two
requires deep knowledge of this leaky views issue. The DBA needs to inspect
any function he wants to mark as leak-free closely, and understand that
innocent-looking things like casts can cause leaks. That is not feasible in
practice. Option 1, however, requires no such knowledge. Operators used in
indexes are already expected to not throw errors, or you would get errors
when inserting certain values to the table, for example.
I might misread his description at first.
Hmm. If we introduce DBA the scenario and the condition to push down qualifiers,
it may be possible to explain more simply.
A challenge of this approach is to determine what qualifier shall be
used to index
accesses in the stage of distribute_qual_to_rels(); prior to the
optimizer's selection
of access methods.
Do you have any good idea, or suggestion?
Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
The attached patch is a revised one; that utilizes untransformRelOptions()
to construct a list of DefElem to be supplied into AT_ResetRelOptions
commands. It enabled me to implement more compact as I expected.
How about this approach to reset existing reloptions?
I'll consolidate part-0, 1 and 2 patches after we make fix the direction to
distinguish leaky qualifiers from others, in the thread of part-2.
Right now, I'm considering the right way to choose qualifiers to be
transformed into index scans.
Thanks,
2011/7/7 Noah Misch <noah@2ndquadrant.com>:
On Thu, Jul 07, 2011 at 03:56:26PM +0100, Kohei KaiGai wrote:
2011/7/7 Noah Misch <noah@2ndquadrant.com>:
On Wed, Jul 06, 2011 at 10:25:12PM +0200, Kohei KaiGai wrote:
*** a/src/backend/commands/view.c --- b/src/backend/commands/view.c--- 227,257 ---- atcmd->def = (Node *) lfirst(c); atcmds = lappend(atcmds, atcmd); } }/* + * If optional parameters are specified, we must set options + * using ALTER TABLE SET OPTION internally. + */ + if (list_length(options) > 0) + { + atcmd = makeNode(AlterTableCmd); + atcmd->subtype = AT_SetRelOptions; + atcmd->def = (List *)options; + + atcmds = lappend(atcmds, atcmd); + } + else + { + atcmd = makeNode(AlterTableCmd); + atcmd->subtype = AT_ResetRelOptions; + atcmd->def = (Node *) list_make1(makeDefElem("security_barrier", + NULL)); + } + if (atcmds != NIL) + AlterTableInternal(viewOid, atcmds, true); + + /* * Seems okay, so return the OID of the pre-existing view. */ relation_close(rel, NoLock); /* keep the lock! */That gets the job done for today, but DefineVirtualRelation() should not need
to know all view options by name to simply replace the existing list with a
new one. I don't think you can cleanly use the ALTER TABLE SET/RESET code for
this. Instead, compute an option list similar to how DefineRelation() does so
at tablecmds.c:491, then update pg_class.My opinion is ALTER TABLE SET/RESET code should be enhanced to accept
an operation to reset all the existing options, rather than tricky
updates of pg_class.The pg_class update has ~20 lines of idiomatic code; see tablecmds.c:7931-7951.
How about an idea to add AT_ResetAllRelOptions for internal use only?
If some operation is purely internal and does not otherwise benefit from the
ALTER TABLE infrastructure, there's no benefit in involving ALTER TABLE.
DefineVirtualRelation() uses ALTER TABLE to add columns because all that code
needs to exist anyway. You could make a plain function to do the update that
gets called from both ATExecSetRelOptions() and DefineVirtualRelation().Thanks,
nm
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
Attachments:
pgsql-v9.2-fix-leaky-view-part-0.v5.patchtext/x-patch; charset=US-ASCII; name=pgsql-v9.2-fix-leaky-view-part-0.v5.patchDownload
*** a/doc/src/sgml/ref/alter_view.sgml
--- b/doc/src/sgml/ref/alter_view.sgml
***************
*** 26,31 **** ALTER VIEW <replaceable class="parameter">name</replaceable> ALTER [ COLUMN ] <r
--- 26,34 ----
ALTER VIEW <replaceable class="parameter">name</replaceable> OWNER TO <replaceable class="PARAMETER">new_owner</replaceable>
ALTER VIEW <replaceable class="parameter">name</replaceable> RENAME TO <replaceable class="parameter">new_name</replaceable>
ALTER VIEW <replaceable class="parameter">name</replaceable> SET SCHEMA <replaceable class="parameter">new_schema</replaceable>
+ ALTER VIEW <replaceable class="parameter">name</replaceable> SET ( <replaceable class="parameter">parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )
+ ALTER VIEW <replaceable class="parameter">name</replaceable> RESET ( <replaceable class="parameter">parameter</replaceable> [, ... ] )
+
</synopsis>
</refsynopsisdiv>
***************
*** 102,107 **** ALTER VIEW <replaceable class="parameter">name</replaceable> SET SCHEMA <replace
--- 105,128 ----
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><replaceable class="parameter">parameter</replaceable></term>
+ <listitem>
+ <para>
+ Name of the view option to be set.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><replaceable class="parameter">value</replaceable></term>
+ <listitem>
+ <para>
+ The new value for the view option.
+ </para>
+ </listitem>
+ </varlistentry>
</variablelist>
</refsect1>
*** a/doc/src/sgml/ref/create_view.sgml
--- b/doc/src/sgml/ref/create_view.sgml
***************
*** 22,27 **** PostgreSQL documentation
--- 22,28 ----
<refsynopsisdiv>
<synopsis>
CREATE [ OR REPLACE ] [ TEMP | TEMPORARY ] VIEW <replaceable class="PARAMETER">name</replaceable> [ ( <replaceable class="PARAMETER">column_name</replaceable> [, ...] ) ]
+ [ WITH ( <replaceable class="PARAMETER">parameter</replaceable> [= <replaceable class="PARAMETER">value</replaceable>] [, ... ] ) ]
AS <replaceable class="PARAMETER">query</replaceable>
</synopsis>
</refsynopsisdiv>
***************
*** 99,104 **** CREATE [ OR REPLACE ] [ TEMP | TEMPORARY ] VIEW <replaceable class="PARAMETER">n
--- 100,128 ----
</varlistentry>
<varlistentry>
+ <term><literal>WITH (<replaceable class="parameter">parameter [= value]</replaceable>)</literal></term>
+ <listitem>
+ <para>
+ This clause allows to specify optional parameters for a view.
+ </para>
+ <para>
+ If <literal>security_barrier=TRUE</literal> is specified, this view
+ shall performs as security barrier that prevent unexpected information
+ leaks. It is a recommendable configuration when the view is defined
+ to apply row-level security, in spite of performance trade-off.
+ </para>
+ <para>
+ It is a commonly-used technique that using views to filter out
+ tuple to be invisible to particular users, however, please note
+ that here is a known-problem that allows malicious users to
+ reference invisible tuples using a function with side-effect
+ because of interaction with query optimization.
+ See <xref linkend="rules-privileges"> for more detailed scenario.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
<term><replaceable class="parameter">query</replaceable></term>
<listitem>
<para>
*** a/src/backend/access/common/reloptions.c
--- b/src/backend/access/common/reloptions.c
***************
*** 66,71 **** static relopt_bool boolRelOpts[] =
--- 66,79 ----
},
true
},
+ {
+ {
+ "security_barrier",
+ "Prevent information leaks using functions with side-effects",
+ RELOPT_KIND_VIEW
+ },
+ false
+ },
/* list terminator */
{{NULL}}
};
***************
*** 776,781 **** extractRelOptions(HeapTuple tuple, TupleDesc tupdesc, Oid amoptions)
--- 784,790 ----
{
case RELKIND_RELATION:
case RELKIND_TOASTVALUE:
+ case RELKIND_VIEW:
case RELKIND_UNCATALOGED:
options = heap_reloptions(classForm->relkind, datum, false);
break;
***************
*** 1134,1140 **** default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
{"autovacuum_vacuum_scale_factor", RELOPT_TYPE_REAL,
offsetof(StdRdOptions, autovacuum) +offsetof(AutoVacOpts, vacuum_scale_factor)},
{"autovacuum_analyze_scale_factor", RELOPT_TYPE_REAL,
! offsetof(StdRdOptions, autovacuum) +offsetof(AutoVacOpts, analyze_scale_factor)}
};
options = parseRelOptions(reloptions, validate, kind, &numoptions);
--- 1143,1151 ----
{"autovacuum_vacuum_scale_factor", RELOPT_TYPE_REAL,
offsetof(StdRdOptions, autovacuum) +offsetof(AutoVacOpts, vacuum_scale_factor)},
{"autovacuum_analyze_scale_factor", RELOPT_TYPE_REAL,
! offsetof(StdRdOptions, autovacuum) +offsetof(AutoVacOpts, analyze_scale_factor)},
! {"security_barrier", RELOPT_TYPE_BOOL,
! offsetof(StdRdOptions, security_barrier)},
};
options = parseRelOptions(reloptions, validate, kind, &numoptions);
***************
*** 1176,1181 **** heap_reloptions(char relkind, Datum reloptions, bool validate)
--- 1187,1194 ----
return (bytea *) rdopts;
case RELKIND_RELATION:
return default_reloptions(reloptions, validate, RELOPT_KIND_HEAP);
+ case RELKIND_VIEW:
+ return default_reloptions(reloptions, validate, RELOPT_KIND_VIEW);
default:
/* other relkinds are not supported */
return NULL;
*** a/src/backend/commands/tablecmds.c
--- b/src/backend/commands/tablecmds.c
***************
*** 2955,2961 **** ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
break;
case AT_SetRelOptions: /* SET (...) */
case AT_ResetRelOptions: /* RESET (...) */
! ATSimplePermissions(rel, ATT_TABLE | ATT_INDEX);
/* This command never recurses */
/* No command-specific prep needed */
pass = AT_PASS_MISC;
--- 2955,2961 ----
break;
case AT_SetRelOptions: /* SET (...) */
case AT_ResetRelOptions: /* RESET (...) */
! ATSimplePermissions(rel, ATT_TABLE | ATT_INDEX | ATT_VIEW);
/* This command never recurses */
/* No command-specific prep needed */
pass = AT_PASS_MISC;
***************
*** 7911,7916 **** ATExecSetRelOptions(Relation rel, List *defList, bool isReset, LOCKMODE lockmode
--- 7911,7917 ----
{
case RELKIND_RELATION:
case RELKIND_TOASTVALUE:
+ case RELKIND_VIEW:
(void) heap_reloptions(rel->rd_rel->relkind, newOptions, true);
break;
case RELKIND_INDEX:
*** a/src/backend/commands/view.c
--- b/src/backend/commands/view.c
***************
*** 15,20 ****
--- 15,21 ----
#include "postgres.h"
#include "access/heapam.h"
+ #include "access/reloptions.h"
#include "access/xact.h"
#include "catalog/namespace.h"
#include "commands/defrem.h"
***************
*** 32,37 ****
--- 33,39 ----
#include "utils/builtins.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
+ #include "utils/syscache.h"
static void checkViewTupleDesc(TupleDesc newdesc, TupleDesc olddesc);
***************
*** 98,104 **** isViewOnTempTable_walker(Node *node, void *context)
*/
static Oid
DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace,
! Oid namespaceId)
{
Oid viewOid;
CreateStmt *createStmt = makeNode(CreateStmt);
--- 100,106 ----
*/
static Oid
DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace,
! Oid namespaceId, List *options)
{
Oid viewOid;
CreateStmt *createStmt = makeNode(CreateStmt);
***************
*** 166,171 **** DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace,
--- 168,178 ----
{
Relation rel;
TupleDesc descriptor;
+ List *atcmds = NIL;
+ AlterTableCmd *atcmd;
+ HeapTuple tuple;
+ Datum datum;
+ bool isnull;
/*
* Yes. Get exclusive lock on the existing view ...
***************
*** 210,223 **** DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace,
*/
if (list_length(attrList) > rel->rd_att->natts)
{
- List *atcmds = NIL;
ListCell *c;
int skip = rel->rd_att->natts;
foreach(c, attrList)
{
- AlterTableCmd *atcmd;
-
if (skip > 0)
{
skip--;
--- 217,227 ----
***************
*** 228,237 **** DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace,
atcmd->def = (Node *) lfirst(c);
atcmds = lappend(atcmds, atcmd);
}
- AlterTableInternal(viewOid, atcmds, true);
}
/*
* Seems okay, so return the OID of the pre-existing view.
*/
relation_close(rel, NoLock); /* keep the lock! */
--- 232,281 ----
atcmd->def = (Node *) lfirst(c);
atcmds = lappend(atcmds, atcmd);
}
}
/*
+ * If existing view has any optional parameters, we shall reset
+ * them at first using ALTER TABLE RESET OPTION internally.
+ */
+ tuple = SearchSysCache1(RELOID, DatumGetObjectId(viewOid));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for relation %u", viewOid);
+ datum = SysCacheGetAttr(RELOID,
+ tuple,
+ Anum_pg_class_reloptions,
+ &isnull);
+ if (!isnull)
+ {
+ List *cur_opts = untransformRelOptions(datum);
+ ListCell *cell;
+
+ atcmd = makeNode(AlterTableCmd);
+ atcmd->subtype = AT_ResetRelOptions;
+ /* Make sure arguments being NULL */
+ foreach (cell, cur_opts)
+ ((DefElem *) lfirst(cell))->arg = NULL;
+ atcmd->def = (Node *)cur_opts;
+ atcmds = lappend(atcmds, atcmd);
+ }
+ ReleaseSysCache(tuple);
+
+ /*
+ * If optional parameters are specified, we must set options
+ * using ALTER TABLE SET OPTION internally.
+ */
+ if (list_length(options) > 0)
+ {
+ atcmd = makeNode(AlterTableCmd);
+ atcmd->subtype = AT_SetRelOptions;
+ atcmd->def = (Node *)options;
+ atcmds = lappend(atcmds, atcmd);
+ }
+
+ if (atcmds != NIL)
+ AlterTableInternal(viewOid, atcmds, true);
+
+ /*
* Seems okay, so return the OID of the pre-existing view.
*/
relation_close(rel, NoLock); /* keep the lock! */
***************
*** 255,260 **** DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace,
--- 299,307 ----
createStmt->tablespacename = NULL;
createStmt->if_not_exists = false;
+ if (options != NIL)
+ createStmt->options = list_concat(createStmt->options, options);
+
/*
* finally create the relation (this will error out if there's an
* existing view, so we don't need more code to complain if "replace"
***************
*** 513,519 **** DefineView(ViewStmt *stmt, const char *queryString)
* aborted.
*/
viewOid = DefineVirtualRelation(view, viewParse->targetList,
! stmt->replace, namespaceId);
/*
* The relation we have just created is not visible to any other commands
--- 560,567 ----
* aborted.
*/
viewOid = DefineVirtualRelation(view, viewParse->targetList,
! stmt->replace, namespaceId,
! stmt->options);
/*
* The relation we have just created is not visible to any other commands
*** a/src/backend/nodes/copyfuncs.c
--- b/src/backend/nodes/copyfuncs.c
***************
*** 1944,1949 **** _copyRangeTblEntry(RangeTblEntry *from)
--- 1944,1950 ----
COPY_SCALAR_FIELD(relid);
COPY_SCALAR_FIELD(relkind);
COPY_NODE_FIELD(subquery);
+ COPY_SCALAR_FIELD(security_barrier);
COPY_SCALAR_FIELD(jointype);
COPY_NODE_FIELD(joinaliasvars);
COPY_NODE_FIELD(funcexpr);
*** a/src/backend/nodes/equalfuncs.c
--- b/src/backend/nodes/equalfuncs.c
***************
*** 2303,2308 **** _equalRangeTblEntry(RangeTblEntry *a, RangeTblEntry *b)
--- 2303,2309 ----
COMPARE_SCALAR_FIELD(relid);
COMPARE_SCALAR_FIELD(relkind);
COMPARE_NODE_FIELD(subquery);
+ COMPARE_SCALAR_FIELD(security_barrier);
COMPARE_SCALAR_FIELD(jointype);
COMPARE_NODE_FIELD(joinaliasvars);
COMPARE_NODE_FIELD(funcexpr);
*** a/src/backend/nodes/outfuncs.c
--- b/src/backend/nodes/outfuncs.c
***************
*** 2311,2316 **** _outRangeTblEntry(StringInfo str, RangeTblEntry *node)
--- 2311,2317 ----
break;
case RTE_SUBQUERY:
WRITE_NODE_FIELD(subquery);
+ WRITE_BOOL_FIELD(security_barrier);
break;
case RTE_JOIN:
WRITE_ENUM_FIELD(jointype, JoinType);
*** a/src/backend/nodes/readfuncs.c
--- b/src/backend/nodes/readfuncs.c
***************
*** 1191,1196 **** _readRangeTblEntry(void)
--- 1191,1197 ----
break;
case RTE_SUBQUERY:
READ_NODE_FIELD(subquery);
+ READ_BOOL_FIELD(security_barrier);
break;
case RTE_JOIN:
READ_ENUM_FIELD(jointype, JoinType);
*** a/src/backend/parser/gram.y
--- b/src/backend/parser/gram.y
***************
*** 7266,7291 **** transaction_mode_list_or_empty:
*
*****************************************************************************/
! ViewStmt: CREATE OptTemp VIEW qualified_name opt_column_list
AS SelectStmt opt_check_option
{
ViewStmt *n = makeNode(ViewStmt);
n->view = $4;
n->view->relpersistence = $2;
n->aliases = $5;
! n->query = $7;
n->replace = false;
$$ = (Node *) n;
}
! | CREATE OR REPLACE OptTemp VIEW qualified_name opt_column_list
AS SelectStmt opt_check_option
{
ViewStmt *n = makeNode(ViewStmt);
n->view = $6;
n->view->relpersistence = $4;
n->aliases = $7;
! n->query = $9;
n->replace = true;
$$ = (Node *) n;
}
;
--- 7266,7293 ----
*
*****************************************************************************/
! ViewStmt: CREATE OptTemp VIEW qualified_name opt_column_list opt_reloptions
AS SelectStmt opt_check_option
{
ViewStmt *n = makeNode(ViewStmt);
n->view = $4;
n->view->relpersistence = $2;
n->aliases = $5;
! n->query = $8;
n->replace = false;
+ n->options = $6;
$$ = (Node *) n;
}
! | CREATE OR REPLACE OptTemp VIEW qualified_name opt_column_list opt_reloptions
AS SelectStmt opt_check_option
{
ViewStmt *n = makeNode(ViewStmt);
n->view = $6;
n->view->relpersistence = $4;
n->aliases = $7;
! n->query = $10;
n->replace = true;
+ n->options = $8;
$$ = (Node *) n;
}
;
*** a/src/backend/rewrite/rewriteHandler.c
--- b/src/backend/rewrite/rewriteHandler.c
***************
*** 1383,1388 **** ApplyRetrieveRule(Query *parsetree,
--- 1383,1389 ----
rte->rtekind = RTE_SUBQUERY;
rte->relid = InvalidOid;
rte->subquery = rule_action;
+ rte->security_barrier = RelationIsSecurityView(relation);
rte->inh = false; /* must not be set for a subquery */
/*
*** a/src/backend/utils/cache/relcache.c
--- b/src/backend/utils/cache/relcache.c
***************
*** 377,382 **** RelationParseRelOptions(Relation relation, HeapTuple tuple)
--- 377,383 ----
case RELKIND_RELATION:
case RELKIND_TOASTVALUE:
case RELKIND_INDEX:
+ case RELKIND_VIEW:
break;
default:
return;
*** a/src/bin/pg_dump/pg_dump.c
--- b/src/bin/pg_dump/pg_dump.c
***************
*** 12023,12030 **** dumpTableSchema(Archive *fout, TableInfo *tbinfo)
if (binary_upgrade)
binary_upgrade_set_pg_class_oids(q, tbinfo->dobj.catId.oid, false);
! appendPQExpBuffer(q, "CREATE VIEW %s AS\n %s\n",
! fmtId(tbinfo->dobj.name), viewdef);
appendPQExpBuffer(labelq, "VIEW %s",
fmtId(tbinfo->dobj.name));
--- 12023,12032 ----
if (binary_upgrade)
binary_upgrade_set_pg_class_oids(q, tbinfo->dobj.catId.oid, false);
! appendPQExpBuffer(q, "CREATE VIEW %s", fmtId(tbinfo->dobj.name));
! if (tbinfo->reloptions && strlen(tbinfo->reloptions) > 0)
! appendPQExpBuffer(q, " WITH (%s)", tbinfo->reloptions);
! appendPQExpBuffer(q, " AS\n %s\n", viewdef);
appendPQExpBuffer(labelq, "VIEW %s",
fmtId(tbinfo->dobj.name));
*** a/src/include/access/reloptions.h
--- b/src/include/access/reloptions.h
***************
*** 42,49 **** typedef enum relopt_kind
RELOPT_KIND_GIST = (1 << 5),
RELOPT_KIND_ATTRIBUTE = (1 << 6),
RELOPT_KIND_TABLESPACE = (1 << 7),
/* if you add a new kind, make sure you update "last_default" too */
! RELOPT_KIND_LAST_DEFAULT = RELOPT_KIND_TABLESPACE,
/* some compilers treat enums as signed ints, so we can't use 1 << 31 */
RELOPT_KIND_MAX = (1 << 30)
} relopt_kind;
--- 42,50 ----
RELOPT_KIND_GIST = (1 << 5),
RELOPT_KIND_ATTRIBUTE = (1 << 6),
RELOPT_KIND_TABLESPACE = (1 << 7),
+ RELOPT_KIND_VIEW = (1 << 8),
/* if you add a new kind, make sure you update "last_default" too */
! RELOPT_KIND_LAST_DEFAULT = RELOPT_KIND_VIEW,
/* some compilers treat enums as signed ints, so we can't use 1 << 31 */
RELOPT_KIND_MAX = (1 << 30)
} relopt_kind;
*** a/src/include/nodes/parsenodes.h
--- b/src/include/nodes/parsenodes.h
***************
*** 706,711 **** typedef struct RangeTblEntry
--- 706,713 ----
*/
Query *subquery; /* the sub-query */
+ bool security_barrier; /* Was a security barrier view? */
+
/*
* Fields valid for a join RTE (else NULL/zero):
*
***************
*** 2339,2344 **** typedef struct ViewStmt
--- 2341,2347 ----
List *aliases; /* target column names */
Node *query; /* the SELECT query */
bool replace; /* replace an existing view? */
+ List *options; /* options from WITH clause */
} ViewStmt;
/* ----------------------
*** a/src/include/utils/rel.h
--- b/src/include/utils/rel.h
***************
*** 168,174 **** typedef struct RelationData
/*
* StdRdOptions
! * Standard contents of rd_options for heaps and generic indexes.
*
* RelationGetFillFactor() and RelationGetTargetPageFreeSpace() can only
* be applied to relations that use this format or a superset for
--- 168,174 ----
/*
* StdRdOptions
! * Standard contents of rd_options for heaps, views and generic indexes.
*
* RelationGetFillFactor() and RelationGetTargetPageFreeSpace() can only
* be applied to relations that use this format or a superset for
***************
*** 194,199 **** typedef struct StdRdOptions
--- 194,200 ----
int32 vl_len_; /* varlena header (do not touch directly!) */
int fillfactor; /* page fill factor in percent (0..100) */
AutoVacOpts autovacuum; /* autovacuum-related options */
+ bool security_barrier; /* performs as security-barrier view */
} StdRdOptions;
#define HEAP_MIN_FILLFACTOR 10
***************
*** 222,227 **** typedef struct StdRdOptions
--- 223,236 ----
(BLCKSZ * (100 - RelationGetFillFactor(relation, defaultff)) / 100)
/*
+ * RelationIsSecurityView
+ * Returns whether the relation is security view, or not
+ */
+ #define RelationIsSecurityView(relation) \
+ ((relation)->rd_options ? \
+ ((StdRdOptions *) (relation)->rd_options)->security_barrier : false)
+
+ /*
* RelationIsValid
* True iff relation descriptor is valid.
*/
*** a/src/test/regress/expected/create_view.out
--- b/src/test/regress/expected/create_view.out
***************
*** 239,244 **** And relnamespace IN (SELECT OID FROM pg_namespace WHERE nspname LIKE 'pg_temp%')
--- 239,282 ----
1
(1 row)
+ --Should work correctly to leaky-view scenario
+ CREATE TABLE lvtest1 (a int, b text);
+ CREATE TABLE lvtest2 (x int, y text);
+ INSERT INTO lvtest1 VALUES (10, 'aaa'), (11, 'bbb'), (12, 'ccc'), (13, 'ddd');
+ INSERT INTO lvtest2 VALUES (11, 'xxx'), (12, 'yyy'), (13, 'zzz'), (14, 'xyz');
+ CREATE OR REPLACE VIEW leaky_v1 WITH (security_barrier=true) AS
+ SELECT * FROM lvtest1 JOIN lvtest2 ON a = x WHERE a % 2 = 0; -- OK
+ CREATE OR REPLACE VIEW leaky_v2 WITH (security_barrier=false) AS
+ SELECT * FROM lvtest1 JOIN lvtest2 ON a = x WHERE a % 2 = 1; -- OK
+ CREATE OR REPLACE VIEW leaky_v3 WITH (invalid_option=1234) AS
+ SELECT * FROM lvtest1 JOIN lvtest2 ON a = x WHERE a % 2 = 1; -- Fail
+ ERROR: unrecognized parameter "invalid_option"
+ SELECT relname, reloptions FROM pg_class WHERE relname like 'leaky_v%';
+ relname | reloptions
+ ----------+--------------------------
+ leaky_v1 | {security_barrier=true}
+ leaky_v2 | {security_barrier=false}
+ (2 rows)
+
+ ALTER TABLE leaky_v1 RESET ( security_barrier );
+ ALTER TABLE leaky_v2 SET ( security_barrier );
+ SELECT relname, reloptions FROM pg_class WHERE relname like 'leaky_v%';
+ relname | reloptions
+ ----------+-------------------------
+ leaky_v1 |
+ leaky_v2 | {security_barrier=true}
+ (2 rows)
+
+ -- reloptions should be reset
+ CREATE OR REPLACE VIEW leaky_v2 AS
+ SELECT * FROM lvtest1 JOIN lvtest2 ON a = x WHERE a % 2 = 1;
+ SELECT relname, reloptions FROM pg_class WHERE relname like 'leaky_v%';
+ relname | reloptions
+ ----------+------------
+ leaky_v1 |
+ leaky_v2 |
+ (2 rows)
+
DROP SCHEMA temp_view_test CASCADE;
NOTICE: drop cascades to 22 other objects
DETAIL: drop cascades to table temp_view_test.base_table
***************
*** 264,270 **** drop cascades to view temp_view_test.v8
drop cascades to sequence temp_view_test.seq1
drop cascades to view temp_view_test.v9
DROP SCHEMA testviewschm2 CASCADE;
! NOTICE: drop cascades to 16 other objects
DETAIL: drop cascades to table t1
drop cascades to view temporal1
drop cascades to view temporal2
--- 302,308 ----
drop cascades to sequence temp_view_test.seq1
drop cascades to view temp_view_test.v9
DROP SCHEMA testviewschm2 CASCADE;
! NOTICE: drop cascades to 20 other objects
DETAIL: drop cascades to table t1
drop cascades to view temporal1
drop cascades to view temporal2
***************
*** 281,284 **** drop cascades to table tbl3
--- 319,326 ----
drop cascades to table tbl4
drop cascades to view mytempview
drop cascades to view pubview
+ drop cascades to table lvtest1
+ drop cascades to table lvtest2
+ drop cascades to view leaky_v1
+ drop cascades to view leaky_v2
SET search_path to public;
*** a/src/test/regress/sql/create_view.sql
--- b/src/test/regress/sql/create_view.sql
***************
*** 191,196 **** AND NOT EXISTS (SELECT g FROM tbl4 LEFT JOIN tmptbl ON tbl4.h = tmptbl.j);
--- 191,220 ----
SELECT count(*) FROM pg_class where relname LIKE 'mytempview'
And relnamespace IN (SELECT OID FROM pg_namespace WHERE nspname LIKE 'pg_temp%');
+ --Should work correctly to leaky-view scenario
+ CREATE TABLE lvtest1 (a int, b text);
+ CREATE TABLE lvtest2 (x int, y text);
+
+ INSERT INTO lvtest1 VALUES (10, 'aaa'), (11, 'bbb'), (12, 'ccc'), (13, 'ddd');
+ INSERT INTO lvtest2 VALUES (11, 'xxx'), (12, 'yyy'), (13, 'zzz'), (14, 'xyz');
+
+ CREATE OR REPLACE VIEW leaky_v1 WITH (security_barrier=true) AS
+ SELECT * FROM lvtest1 JOIN lvtest2 ON a = x WHERE a % 2 = 0; -- OK
+ CREATE OR REPLACE VIEW leaky_v2 WITH (security_barrier=false) AS
+ SELECT * FROM lvtest1 JOIN lvtest2 ON a = x WHERE a % 2 = 1; -- OK
+ CREATE OR REPLACE VIEW leaky_v3 WITH (invalid_option=1234) AS
+ SELECT * FROM lvtest1 JOIN lvtest2 ON a = x WHERE a % 2 = 1; -- Fail
+ SELECT relname, reloptions FROM pg_class WHERE relname like 'leaky_v%';
+
+ ALTER TABLE leaky_v1 RESET ( security_barrier );
+ ALTER TABLE leaky_v2 SET ( security_barrier );
+ SELECT relname, reloptions FROM pg_class WHERE relname like 'leaky_v%';
+
+ -- reloptions should be reset
+ CREATE OR REPLACE VIEW leaky_v2 AS
+ SELECT * FROM lvtest1 JOIN lvtest2 ON a = x WHERE a % 2 = 1;
+ SELECT relname, reloptions FROM pg_class WHERE relname like 'leaky_v%';
+
DROP SCHEMA temp_view_test CASCADE;
DROP SCHEMA testviewschm2 CASCADE;
On Fri, Jul 8, 2011 at 4:18 AM, Heikki Linnakangas
<heikki.linnakangas@enterprisedb.com> wrote:
IMHO the situation from DBA's point of view is exactly opposite. Option two
requires deep knowledge of this leaky views issue. The DBA needs to inspect
any function he wants to mark as leak-free closely, and understand that
innocent-looking things like casts can cause leaks. That is not feasible in
practice. Option 1, however, requires no such knowledge. Operators used in
indexes are already expected to not throw errors, or you would get errors
when inserting certain values to the table, for example.
But, IMHO, the chance of the DBA wanting to set this flag is
miniscule. I think that 99.9% of DBAs will be perfectly happy to just
use whatever set we mark as built-ins. And an explicit pg_proc flag
gives us a lot more flexibility.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
On Fri, Jul 08, 2011 at 09:20:46AM +0100, Kohei KaiGai wrote:
2011/7/7 Noah Misch <noah@2ndquadrant.com>:
On Thu, Jul 07, 2011 at 03:56:26PM +0100, Kohei KaiGai wrote:
2011/7/7 Noah Misch <noah@2ndquadrant.com>:
On Wed, Jul 06, 2011 at 10:25:12PM +0200, Kohei KaiGai wrote:
That gets the job done for today, but DefineVirtualRelation() should not need
to know all view options by name to simply replace the existing list with a
new one. ?I don't think you can cleanly use the ALTER TABLE SET/RESET code for
this. ?Instead, compute an option list similar to how DefineRelation() does so
at tablecmds.c:491, then update pg_class.My opinion is ALTER TABLE SET/RESET code should be enhanced to accept
an operation to reset all the existing options, rather than tricky
updates of pg_class.The pg_class update has ~20 lines of idiomatic code; see tablecmds.c:7931-7951.
Even if idiomatic, another part of DefineVirtualRelation() uses
AlterTableInternal().
I think a common way is more straightforward.
The fact that we use ALTER TABLE ADD COLUMN in DefineVirtualRelation() is not
itself cause to use ALTER TABLE SET (...) nearby. We should do so only if it
brings some advantage, like simpler or more-robust code. I'm not seeing either
advantage. Those can be points of style, so perhaps I have the poor taste here.
So, how about an idea to add a function that pull-out existing options
from syscache,
and merge with the supplied options list prior to AlterTableInternal()?
It seems wrong to me to trawl through the view's existing option list just to
replace it completely with a new option list. Again, it's subjective. If you'd
like to proceed with this and let the committer decide, that's fine with me.
Thanks,
nm
On Fri, Jul 08, 2011 at 10:09:54AM +0100, Kohei KaiGai wrote:
2011/7/8 Heikki Linnakangas <heikki.linnakangas@enterprisedb.com>:
On 08.07.2011 11:03, Kohei KaiGai wrote:
2011/7/7 Noah Misch<noah@2ndquadrant.com>:
Making a distinction based simply on the call being an operator vs. a
function
is a dead end. ?I see these options:1. The user defining a security view can be assumed to trust the operator
class
members of indexes defined on the tables he references. ?Keep track of
which
those are and treat only them as non-leakable. ?This covers many
interesting
cases, but it's probably tricky to implement and/or costly at runtime.It requires DBA massive amount of detailed knowledge about functions
underlying
operators used in a view. I don't think it is a realistic assumption.2. Add a pg_proc flag indicating whether the function is known leak-free.
Simple, but tedious and perhaps error-prone.+1
IMHO the situation from DBA's point of view is exactly opposite. Option two
requires deep knowledge of this leaky views issue. The DBA needs to inspect
any function he wants to mark as leak-free closely, and understand that
innocent-looking things like casts can cause leaks. That is not feasible in
practice. Option 1, however, requires no such knowledge. Operators used in
indexes are already expected to not throw errors, or you would get errors
when inserting certain values to the table, for example.I might misread his description at first.
Hmm. If we introduce DBA the scenario and the condition to push down qualifiers,
it may be possible to explain more simply.A challenge of this approach is to determine what qualifier shall be
used to index
accesses in the stage of distribute_qual_to_rels(); prior to the
optimizer's selection
of access methods.
Do you have any good idea, or suggestion?
Note that it does not matter whether we're actually doing an index scan -- a seq
scan with a filter using only leakproof operators is equally acceptable. What I
had in mind was to enumerate all operators in operator classes of indexes below
each security view. Those become the leak-free operators for that security
view. If the operator for an OpExpr is considered leak-free by all sources of
its operands, then we may push it down. That's purely a high-level sketch: I
haven't considered implementation concerns in any detail. The resulting
behavior could be surprising: adding an index may change a plan without the new
plan actually using the index.
I lean toward favoring the pg_proc flag. Functions like "texteq" will be taken
as leakproof even if no involved table has an index on a text column. It works
for functions that will never take a place in an operator class, like
length(text). When a user reports a qualifier not getting pushed down, the
answer is much more satisfying: "Run 'CREATE OR REPLACE FUNCTION
... I_DONT_LEAK' as a superuser." Compare to "Define an operator class that
includes the function, if needed, and create an otherwise-useless index." The
main disadvantage I see is the loss of policy locality. Only a superuser (or
maybe database owner?) can create or modify declared-leakproof functions, and
that decision applies throughout the database. However, I think the other
advantages clearly outweigh that loss.
Incidentally, whichever policy we choose here can also loosen the constraints on
qualifier order (part 1 of your original submission).
On Fri, Jul 8, 2011 at 4:57 PM, Noah Misch <noah@2ndquadrant.com> wrote:
Note that it does not matter whether we're actually doing an index scan -- a seq
scan with a filter using only leakproof operators is equally acceptable. What I
had in mind was to enumerate all operators in operator classes of indexes below
each security view. Those become the leak-free operators for that security
view. If the operator for an OpExpr is considered leak-free by all sources of
its operands, then we may push it down. That's purely a high-level sketch: I
haven't considered implementation concerns in any detail. The resulting
behavior could be surprising: adding an index may change a plan without the new
plan actually using the index.I lean toward favoring the pg_proc flag. Functions like "texteq" will be taken
as leakproof even if no involved table has an index on a text column. It works
for functions that will never take a place in an operator class, like
length(text). When a user reports a qualifier not getting pushed down, the
answer is much more satisfying: "Run 'CREATE OR REPLACE FUNCTION
... I_DONT_LEAK' as a superuser." Compare to "Define an operator class that
includes the function, if needed, and create an otherwise-useless index." The
main disadvantage I see is the loss of policy locality. Only a superuser (or
maybe database owner?) can create or modify declared-leakproof functions, and
that decision applies throughout the database. However, I think the other
advantages clearly outweigh that loss.
This strikes me as a fairly compelling refutation of Heikki's proposed approach.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
2011/7/8 Noah Misch <noah@2ndquadrant.com>:
On Fri, Jul 08, 2011 at 09:20:46AM +0100, Kohei KaiGai wrote:
2011/7/7 Noah Misch <noah@2ndquadrant.com>:
On Thu, Jul 07, 2011 at 03:56:26PM +0100, Kohei KaiGai wrote:
2011/7/7 Noah Misch <noah@2ndquadrant.com>:
On Wed, Jul 06, 2011 at 10:25:12PM +0200, Kohei KaiGai wrote:
That gets the job done for today, but DefineVirtualRelation() should not need
to know all view options by name to simply replace the existing list with a
new one. ?I don't think you can cleanly use the ALTER TABLE SET/RESET code for
this. ?Instead, compute an option list similar to how DefineRelation() does so
at tablecmds.c:491, then update pg_class.My opinion is ALTER TABLE SET/RESET code should be enhanced to accept
an operation to reset all the existing options, rather than tricky
updates of pg_class.The pg_class update has ~20 lines of idiomatic code; see tablecmds.c:7931-7951.
Even if idiomatic, another part of DefineVirtualRelation() uses
AlterTableInternal().
I think a common way is more straightforward.The fact that we use ALTER TABLE ADD COLUMN in DefineVirtualRelation() is not
itself cause to use ALTER TABLE SET (...) nearby. We should do so only if it
brings some advantage, like simpler or more-robust code. I'm not seeing either
advantage. Those can be points of style, so perhaps I have the poor taste here.
The attached patch is a revised version according to the approach that updates
pg_class system catalog before AlterTableInternal().
It invokes the new ResetViewOptions when rel->rd_options is not null, and it set
null on the pg_class.reloptions of the view and increments command counter.
Rest of stuffs are not changed from the v5.
Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
Attachments:
pgsql-v9.2-fix-leaky-view-part-0.v6.patchapplication/octet-stream; name=pgsql-v9.2-fix-leaky-view-part-0.v6.patchDownload
*** a/doc/src/sgml/ref/alter_view.sgml
--- b/doc/src/sgml/ref/alter_view.sgml
***************
*** 26,31 **** ALTER VIEW <replaceable class="parameter">name</replaceable> ALTER [ COLUMN ] <r
--- 26,34 ----
ALTER VIEW <replaceable class="parameter">name</replaceable> OWNER TO <replaceable class="PARAMETER">new_owner</replaceable>
ALTER VIEW <replaceable class="parameter">name</replaceable> RENAME TO <replaceable class="parameter">new_name</replaceable>
ALTER VIEW <replaceable class="parameter">name</replaceable> SET SCHEMA <replaceable class="parameter">new_schema</replaceable>
+ ALTER VIEW <replaceable class="parameter">name</replaceable> SET ( <replaceable class="parameter">parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )
+ ALTER VIEW <replaceable class="parameter">name</replaceable> RESET ( <replaceable class="parameter">parameter</replaceable> [, ... ] )
+
</synopsis>
</refsynopsisdiv>
***************
*** 102,107 **** ALTER VIEW <replaceable class="parameter">name</replaceable> SET SCHEMA <replace
--- 105,128 ----
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><replaceable class="parameter">parameter</replaceable></term>
+ <listitem>
+ <para>
+ Name of the view option to be set.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><replaceable class="parameter">value</replaceable></term>
+ <listitem>
+ <para>
+ The new value for the view option.
+ </para>
+ </listitem>
+ </varlistentry>
</variablelist>
</refsect1>
*** a/doc/src/sgml/ref/create_view.sgml
--- b/doc/src/sgml/ref/create_view.sgml
***************
*** 22,27 **** PostgreSQL documentation
--- 22,28 ----
<refsynopsisdiv>
<synopsis>
CREATE [ OR REPLACE ] [ TEMP | TEMPORARY ] VIEW <replaceable class="PARAMETER">name</replaceable> [ ( <replaceable class="PARAMETER">column_name</replaceable> [, ...] ) ]
+ [ WITH ( <replaceable class="PARAMETER">parameter</replaceable> [= <replaceable class="PARAMETER">value</replaceable>] [, ... ] ) ]
AS <replaceable class="PARAMETER">query</replaceable>
</synopsis>
</refsynopsisdiv>
***************
*** 99,104 **** CREATE [ OR REPLACE ] [ TEMP | TEMPORARY ] VIEW <replaceable class="PARAMETER">n
--- 100,128 ----
</varlistentry>
<varlistentry>
+ <term><literal>WITH (<replaceable class="parameter">parameter [= value]</replaceable>)</literal></term>
+ <listitem>
+ <para>
+ This clause allows to specify optional parameters for a view.
+ </para>
+ <para>
+ If <literal>security_barrier=TRUE</literal> is specified, this view
+ shall performs as security barrier that prevent unexpected information
+ leaks. It is a recommendable configuration when the view is defined
+ to apply row-level security, in spite of performance trade-off.
+ </para>
+ <para>
+ It is a commonly-used technique that using views to filter out
+ tuple to be invisible to particular users, however, please note
+ that here is a known-problem that allows malicious users to
+ reference invisible tuples using a function with side-effect
+ because of interaction with query optimization.
+ See <xref linkend="rules-privileges"> for more detailed scenario.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
<term><replaceable class="parameter">query</replaceable></term>
<listitem>
<para>
*** a/src/backend/access/common/reloptions.c
--- b/src/backend/access/common/reloptions.c
***************
*** 66,71 **** static relopt_bool boolRelOpts[] =
--- 66,79 ----
},
true
},
+ {
+ {
+ "security_barrier",
+ "Prevent information leaks using functions with side-effects",
+ RELOPT_KIND_VIEW
+ },
+ false
+ },
/* list terminator */
{{NULL}}
};
***************
*** 776,781 **** extractRelOptions(HeapTuple tuple, TupleDesc tupdesc, Oid amoptions)
--- 784,790 ----
{
case RELKIND_RELATION:
case RELKIND_TOASTVALUE:
+ case RELKIND_VIEW:
case RELKIND_UNCATALOGED:
options = heap_reloptions(classForm->relkind, datum, false);
break;
***************
*** 1134,1140 **** default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
{"autovacuum_vacuum_scale_factor", RELOPT_TYPE_REAL,
offsetof(StdRdOptions, autovacuum) +offsetof(AutoVacOpts, vacuum_scale_factor)},
{"autovacuum_analyze_scale_factor", RELOPT_TYPE_REAL,
! offsetof(StdRdOptions, autovacuum) +offsetof(AutoVacOpts, analyze_scale_factor)}
};
options = parseRelOptions(reloptions, validate, kind, &numoptions);
--- 1143,1151 ----
{"autovacuum_vacuum_scale_factor", RELOPT_TYPE_REAL,
offsetof(StdRdOptions, autovacuum) +offsetof(AutoVacOpts, vacuum_scale_factor)},
{"autovacuum_analyze_scale_factor", RELOPT_TYPE_REAL,
! offsetof(StdRdOptions, autovacuum) +offsetof(AutoVacOpts, analyze_scale_factor)},
! {"security_barrier", RELOPT_TYPE_BOOL,
! offsetof(StdRdOptions, security_barrier)},
};
options = parseRelOptions(reloptions, validate, kind, &numoptions);
***************
*** 1176,1181 **** heap_reloptions(char relkind, Datum reloptions, bool validate)
--- 1187,1194 ----
return (bytea *) rdopts;
case RELKIND_RELATION:
return default_reloptions(reloptions, validate, RELOPT_KIND_HEAP);
+ case RELKIND_VIEW:
+ return default_reloptions(reloptions, validate, RELOPT_KIND_VIEW);
default:
/* other relkinds are not supported */
return NULL;
*** a/src/backend/commands/tablecmds.c
--- b/src/backend/commands/tablecmds.c
***************
*** 2955,2961 **** ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
break;
case AT_SetRelOptions: /* SET (...) */
case AT_ResetRelOptions: /* RESET (...) */
! ATSimplePermissions(rel, ATT_TABLE | ATT_INDEX);
/* This command never recurses */
/* No command-specific prep needed */
pass = AT_PASS_MISC;
--- 2955,2961 ----
break;
case AT_SetRelOptions: /* SET (...) */
case AT_ResetRelOptions: /* RESET (...) */
! ATSimplePermissions(rel, ATT_TABLE | ATT_INDEX | ATT_VIEW);
/* This command never recurses */
/* No command-specific prep needed */
pass = AT_PASS_MISC;
***************
*** 7911,7916 **** ATExecSetRelOptions(Relation rel, List *defList, bool isReset, LOCKMODE lockmode
--- 7911,7917 ----
{
case RELKIND_RELATION:
case RELKIND_TOASTVALUE:
+ case RELKIND_VIEW:
(void) heap_reloptions(rel->rd_rel->relkind, newOptions, true);
break;
case RELKIND_INDEX:
*** a/src/backend/commands/view.c
--- b/src/backend/commands/view.c
***************
*** 16,21 ****
--- 16,22 ----
#include "access/heapam.h"
#include "access/xact.h"
+ #include "catalog/indexing.h"
#include "catalog/namespace.h"
#include "commands/defrem.h"
#include "commands/tablecmds.h"
***************
*** 32,37 ****
--- 33,39 ----
#include "utils/builtins.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
+ #include "utils/syscache.h"
static void checkViewTupleDesc(TupleDesc newdesc, TupleDesc olddesc);
***************
*** 87,92 **** isViewOnTempTable_walker(Node *node, void *context)
--- 89,135 ----
context);
}
+ /*
+ * ResetViewOptions
+ *
+ * It clears all the reloptions prior to replacing
+ */
+ static void
+ ResetViewOptions(Oid viewOid)
+ {
+ Relation pg_class;
+ HeapTuple oldtup;
+ HeapTuple newtup;
+ Datum values[Natts_pg_class];
+ bool nulls[Natts_pg_class];
+ bool replaces[Natts_pg_class];
+
+ pg_class = heap_open(RelationRelationId, RowExclusiveLock);
+
+ oldtup = SearchSysCache1(RELOID, DatumGetObjectId(viewOid));
+ if (!HeapTupleIsValid(oldtup))
+ elog(ERROR, "cache lookup failed for relation %u", viewOid);
+
+ memset(values, 0, sizeof(values));
+ memset(nulls, false, sizeof(nulls));
+ memset(replaces, false, sizeof(replaces));
+
+ replaces[Anum_pg_class_reloptions - 1] = true;
+ nulls[Anum_pg_class_reloptions - 1] = true;
+
+ newtup = heap_modify_tuple(oldtup, RelationGetDescr(pg_class),
+ values, nulls, replaces);
+ simple_heap_update(pg_class, &newtup->t_self, newtup);
+
+ CatalogUpdateIndexes(pg_class, newtup);
+
+ ReleaseSysCache(oldtup);
+
+ heap_close(pg_class, RowExclusiveLock);
+
+ CommandCounterIncrement();
+ }
+
/*---------------------------------------------------------------------
* DefineVirtualRelation
*
***************
*** 98,104 **** isViewOnTempTable_walker(Node *node, void *context)
*/
static Oid
DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace,
! Oid namespaceId)
{
Oid viewOid;
CreateStmt *createStmt = makeNode(CreateStmt);
--- 141,147 ----
*/
static Oid
DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace,
! Oid namespaceId, List *options)
{
Oid viewOid;
CreateStmt *createStmt = makeNode(CreateStmt);
***************
*** 166,171 **** DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace,
--- 209,216 ----
{
Relation rel;
TupleDesc descriptor;
+ List *atcmds = NIL;
+ AlterTableCmd *atcmd;
/*
* Yes. Get exclusive lock on the existing view ...
***************
*** 210,223 **** DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace,
*/
if (list_length(attrList) > rel->rd_att->natts)
{
- List *atcmds = NIL;
ListCell *c;
int skip = rel->rd_att->natts;
foreach(c, attrList)
{
- AlterTableCmd *atcmd;
-
if (skip > 0)
{
skip--;
--- 255,265 ----
***************
*** 228,237 **** DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace,
atcmd->def = (Node *) lfirst(c);
atcmds = lappend(atcmds, atcmd);
}
- AlterTableInternal(viewOid, atcmds, true);
}
/*
* Seems okay, so return the OID of the pre-existing view.
*/
relation_close(rel, NoLock); /* keep the lock! */
--- 270,300 ----
atcmd->def = (Node *) lfirst(c);
atcmds = lappend(atcmds, atcmd);
}
}
/*
+ * If existing view has any optional parameters, we shall reset
+ * them prior to setting new values.
+ */
+ if (rel->rd_options)
+ ResetViewOptions(viewOid);
+
+ /*
+ * If optional parameters are specified, we must set options
+ * using ALTER TABLE SET OPTION internally.
+ */
+ if (list_length(options) > 0)
+ {
+ atcmd = makeNode(AlterTableCmd);
+ atcmd->subtype = AT_SetRelOptions;
+ atcmd->def = (Node *)options;
+ atcmds = lappend(atcmds, atcmd);
+ }
+
+ if (atcmds != NIL)
+ AlterTableInternal(viewOid, atcmds, true);
+
+ /*
* Seems okay, so return the OID of the pre-existing view.
*/
relation_close(rel, NoLock); /* keep the lock! */
***************
*** 255,260 **** DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace,
--- 318,326 ----
createStmt->tablespacename = NULL;
createStmt->if_not_exists = false;
+ if (options != NIL)
+ createStmt->options = list_concat(createStmt->options, options);
+
/*
* finally create the relation (this will error out if there's an
* existing view, so we don't need more code to complain if "replace"
***************
*** 513,519 **** DefineView(ViewStmt *stmt, const char *queryString)
* aborted.
*/
viewOid = DefineVirtualRelation(view, viewParse->targetList,
! stmt->replace, namespaceId);
/*
* The relation we have just created is not visible to any other commands
--- 579,586 ----
* aborted.
*/
viewOid = DefineVirtualRelation(view, viewParse->targetList,
! stmt->replace, namespaceId,
! stmt->options);
/*
* The relation we have just created is not visible to any other commands
*** a/src/backend/nodes/copyfuncs.c
--- b/src/backend/nodes/copyfuncs.c
***************
*** 1944,1949 **** _copyRangeTblEntry(RangeTblEntry *from)
--- 1944,1950 ----
COPY_SCALAR_FIELD(relid);
COPY_SCALAR_FIELD(relkind);
COPY_NODE_FIELD(subquery);
+ COPY_SCALAR_FIELD(security_barrier);
COPY_SCALAR_FIELD(jointype);
COPY_NODE_FIELD(joinaliasvars);
COPY_NODE_FIELD(funcexpr);
*** a/src/backend/nodes/equalfuncs.c
--- b/src/backend/nodes/equalfuncs.c
***************
*** 2303,2308 **** _equalRangeTblEntry(RangeTblEntry *a, RangeTblEntry *b)
--- 2303,2309 ----
COMPARE_SCALAR_FIELD(relid);
COMPARE_SCALAR_FIELD(relkind);
COMPARE_NODE_FIELD(subquery);
+ COMPARE_SCALAR_FIELD(security_barrier);
COMPARE_SCALAR_FIELD(jointype);
COMPARE_NODE_FIELD(joinaliasvars);
COMPARE_NODE_FIELD(funcexpr);
*** a/src/backend/nodes/outfuncs.c
--- b/src/backend/nodes/outfuncs.c
***************
*** 2311,2316 **** _outRangeTblEntry(StringInfo str, RangeTblEntry *node)
--- 2311,2317 ----
break;
case RTE_SUBQUERY:
WRITE_NODE_FIELD(subquery);
+ WRITE_BOOL_FIELD(security_barrier);
break;
case RTE_JOIN:
WRITE_ENUM_FIELD(jointype, JoinType);
*** a/src/backend/nodes/readfuncs.c
--- b/src/backend/nodes/readfuncs.c
***************
*** 1191,1196 **** _readRangeTblEntry(void)
--- 1191,1197 ----
break;
case RTE_SUBQUERY:
READ_NODE_FIELD(subquery);
+ READ_BOOL_FIELD(security_barrier);
break;
case RTE_JOIN:
READ_ENUM_FIELD(jointype, JoinType);
*** a/src/backend/parser/gram.y
--- b/src/backend/parser/gram.y
***************
*** 7266,7291 **** transaction_mode_list_or_empty:
*
*****************************************************************************/
! ViewStmt: CREATE OptTemp VIEW qualified_name opt_column_list
AS SelectStmt opt_check_option
{
ViewStmt *n = makeNode(ViewStmt);
n->view = $4;
n->view->relpersistence = $2;
n->aliases = $5;
! n->query = $7;
n->replace = false;
$$ = (Node *) n;
}
! | CREATE OR REPLACE OptTemp VIEW qualified_name opt_column_list
AS SelectStmt opt_check_option
{
ViewStmt *n = makeNode(ViewStmt);
n->view = $6;
n->view->relpersistence = $4;
n->aliases = $7;
! n->query = $9;
n->replace = true;
$$ = (Node *) n;
}
;
--- 7266,7293 ----
*
*****************************************************************************/
! ViewStmt: CREATE OptTemp VIEW qualified_name opt_column_list opt_reloptions
AS SelectStmt opt_check_option
{
ViewStmt *n = makeNode(ViewStmt);
n->view = $4;
n->view->relpersistence = $2;
n->aliases = $5;
! n->query = $8;
n->replace = false;
+ n->options = $6;
$$ = (Node *) n;
}
! | CREATE OR REPLACE OptTemp VIEW qualified_name opt_column_list opt_reloptions
AS SelectStmt opt_check_option
{
ViewStmt *n = makeNode(ViewStmt);
n->view = $6;
n->view->relpersistence = $4;
n->aliases = $7;
! n->query = $10;
n->replace = true;
+ n->options = $8;
$$ = (Node *) n;
}
;
*** a/src/backend/rewrite/rewriteHandler.c
--- b/src/backend/rewrite/rewriteHandler.c
***************
*** 1383,1388 **** ApplyRetrieveRule(Query *parsetree,
--- 1383,1389 ----
rte->rtekind = RTE_SUBQUERY;
rte->relid = InvalidOid;
rte->subquery = rule_action;
+ rte->security_barrier = RelationIsSecurityView(relation);
rte->inh = false; /* must not be set for a subquery */
/*
*** a/src/backend/utils/cache/relcache.c
--- b/src/backend/utils/cache/relcache.c
***************
*** 377,382 **** RelationParseRelOptions(Relation relation, HeapTuple tuple)
--- 377,383 ----
case RELKIND_RELATION:
case RELKIND_TOASTVALUE:
case RELKIND_INDEX:
+ case RELKIND_VIEW:
break;
default:
return;
*** a/src/bin/pg_dump/pg_dump.c
--- b/src/bin/pg_dump/pg_dump.c
***************
*** 12023,12030 **** dumpTableSchema(Archive *fout, TableInfo *tbinfo)
if (binary_upgrade)
binary_upgrade_set_pg_class_oids(q, tbinfo->dobj.catId.oid, false);
! appendPQExpBuffer(q, "CREATE VIEW %s AS\n %s\n",
! fmtId(tbinfo->dobj.name), viewdef);
appendPQExpBuffer(labelq, "VIEW %s",
fmtId(tbinfo->dobj.name));
--- 12023,12032 ----
if (binary_upgrade)
binary_upgrade_set_pg_class_oids(q, tbinfo->dobj.catId.oid, false);
! appendPQExpBuffer(q, "CREATE VIEW %s", fmtId(tbinfo->dobj.name));
! if (tbinfo->reloptions && strlen(tbinfo->reloptions) > 0)
! appendPQExpBuffer(q, " WITH (%s)", tbinfo->reloptions);
! appendPQExpBuffer(q, " AS\n %s\n", viewdef);
appendPQExpBuffer(labelq, "VIEW %s",
fmtId(tbinfo->dobj.name));
*** a/src/include/access/reloptions.h
--- b/src/include/access/reloptions.h
***************
*** 42,49 **** typedef enum relopt_kind
RELOPT_KIND_GIST = (1 << 5),
RELOPT_KIND_ATTRIBUTE = (1 << 6),
RELOPT_KIND_TABLESPACE = (1 << 7),
/* if you add a new kind, make sure you update "last_default" too */
! RELOPT_KIND_LAST_DEFAULT = RELOPT_KIND_TABLESPACE,
/* some compilers treat enums as signed ints, so we can't use 1 << 31 */
RELOPT_KIND_MAX = (1 << 30)
} relopt_kind;
--- 42,50 ----
RELOPT_KIND_GIST = (1 << 5),
RELOPT_KIND_ATTRIBUTE = (1 << 6),
RELOPT_KIND_TABLESPACE = (1 << 7),
+ RELOPT_KIND_VIEW = (1 << 8),
/* if you add a new kind, make sure you update "last_default" too */
! RELOPT_KIND_LAST_DEFAULT = RELOPT_KIND_VIEW,
/* some compilers treat enums as signed ints, so we can't use 1 << 31 */
RELOPT_KIND_MAX = (1 << 30)
} relopt_kind;
*** a/src/include/nodes/parsenodes.h
--- b/src/include/nodes/parsenodes.h
***************
*** 706,711 **** typedef struct RangeTblEntry
--- 706,713 ----
*/
Query *subquery; /* the sub-query */
+ bool security_barrier; /* Was a security barrier view? */
+
/*
* Fields valid for a join RTE (else NULL/zero):
*
***************
*** 2339,2344 **** typedef struct ViewStmt
--- 2341,2347 ----
List *aliases; /* target column names */
Node *query; /* the SELECT query */
bool replace; /* replace an existing view? */
+ List *options; /* options from WITH clause */
} ViewStmt;
/* ----------------------
*** a/src/include/utils/rel.h
--- b/src/include/utils/rel.h
***************
*** 168,174 **** typedef struct RelationData
/*
* StdRdOptions
! * Standard contents of rd_options for heaps and generic indexes.
*
* RelationGetFillFactor() and RelationGetTargetPageFreeSpace() can only
* be applied to relations that use this format or a superset for
--- 168,174 ----
/*
* StdRdOptions
! * Standard contents of rd_options for heaps, views and generic indexes.
*
* RelationGetFillFactor() and RelationGetTargetPageFreeSpace() can only
* be applied to relations that use this format or a superset for
***************
*** 194,199 **** typedef struct StdRdOptions
--- 194,200 ----
int32 vl_len_; /* varlena header (do not touch directly!) */
int fillfactor; /* page fill factor in percent (0..100) */
AutoVacOpts autovacuum; /* autovacuum-related options */
+ bool security_barrier; /* performs as security-barrier view */
} StdRdOptions;
#define HEAP_MIN_FILLFACTOR 10
***************
*** 222,227 **** typedef struct StdRdOptions
--- 223,236 ----
(BLCKSZ * (100 - RelationGetFillFactor(relation, defaultff)) / 100)
/*
+ * RelationIsSecurityView
+ * Returns whether the relation is security view, or not
+ */
+ #define RelationIsSecurityView(relation) \
+ ((relation)->rd_options ? \
+ ((StdRdOptions *) (relation)->rd_options)->security_barrier : false)
+
+ /*
* RelationIsValid
* True iff relation descriptor is valid.
*/
*** a/src/test/regress/expected/create_view.out
--- b/src/test/regress/expected/create_view.out
***************
*** 239,244 **** And relnamespace IN (SELECT OID FROM pg_namespace WHERE nspname LIKE 'pg_temp%')
--- 239,282 ----
1
(1 row)
+ --Should work correctly to leaky-view scenario
+ CREATE TABLE lvtest1 (a int, b text);
+ CREATE TABLE lvtest2 (x int, y text);
+ INSERT INTO lvtest1 VALUES (10, 'aaa'), (11, 'bbb'), (12, 'ccc'), (13, 'ddd');
+ INSERT INTO lvtest2 VALUES (11, 'xxx'), (12, 'yyy'), (13, 'zzz'), (14, 'xyz');
+ CREATE OR REPLACE VIEW leaky_v1 WITH (security_barrier=true) AS
+ SELECT * FROM lvtest1 JOIN lvtest2 ON a = x WHERE a % 2 = 0; -- OK
+ CREATE OR REPLACE VIEW leaky_v2 WITH (security_barrier=false) AS
+ SELECT * FROM lvtest1 JOIN lvtest2 ON a = x WHERE a % 2 = 1; -- OK
+ CREATE OR REPLACE VIEW leaky_v3 WITH (invalid_option=1234) AS
+ SELECT * FROM lvtest1 JOIN lvtest2 ON a = x WHERE a % 2 = 1; -- Fail
+ ERROR: unrecognized parameter "invalid_option"
+ SELECT relname, reloptions FROM pg_class WHERE relname like 'leaky_v%';
+ relname | reloptions
+ ----------+--------------------------
+ leaky_v1 | {security_barrier=true}
+ leaky_v2 | {security_barrier=false}
+ (2 rows)
+
+ ALTER TABLE leaky_v1 RESET ( security_barrier );
+ ALTER TABLE leaky_v2 SET ( security_barrier );
+ SELECT relname, reloptions FROM pg_class WHERE relname like 'leaky_v%';
+ relname | reloptions
+ ----------+-------------------------
+ leaky_v1 |
+ leaky_v2 | {security_barrier=true}
+ (2 rows)
+
+ -- reloptions should be reset
+ CREATE OR REPLACE VIEW leaky_v2 AS
+ SELECT * FROM lvtest1 JOIN lvtest2 ON a = x WHERE a % 2 = 1;
+ SELECT relname, reloptions FROM pg_class WHERE relname like 'leaky_v%';
+ relname | reloptions
+ ----------+------------
+ leaky_v1 |
+ leaky_v2 |
+ (2 rows)
+
DROP SCHEMA temp_view_test CASCADE;
NOTICE: drop cascades to 22 other objects
DETAIL: drop cascades to table temp_view_test.base_table
***************
*** 264,270 **** drop cascades to view temp_view_test.v8
drop cascades to sequence temp_view_test.seq1
drop cascades to view temp_view_test.v9
DROP SCHEMA testviewschm2 CASCADE;
! NOTICE: drop cascades to 16 other objects
DETAIL: drop cascades to table t1
drop cascades to view temporal1
drop cascades to view temporal2
--- 302,308 ----
drop cascades to sequence temp_view_test.seq1
drop cascades to view temp_view_test.v9
DROP SCHEMA testviewschm2 CASCADE;
! NOTICE: drop cascades to 20 other objects
DETAIL: drop cascades to table t1
drop cascades to view temporal1
drop cascades to view temporal2
***************
*** 281,284 **** drop cascades to table tbl3
--- 319,326 ----
drop cascades to table tbl4
drop cascades to view mytempview
drop cascades to view pubview
+ drop cascades to table lvtest1
+ drop cascades to table lvtest2
+ drop cascades to view leaky_v1
+ drop cascades to view leaky_v2
SET search_path to public;
*** a/src/test/regress/sql/create_view.sql
--- b/src/test/regress/sql/create_view.sql
***************
*** 191,196 **** AND NOT EXISTS (SELECT g FROM tbl4 LEFT JOIN tmptbl ON tbl4.h = tmptbl.j);
--- 191,220 ----
SELECT count(*) FROM pg_class where relname LIKE 'mytempview'
And relnamespace IN (SELECT OID FROM pg_namespace WHERE nspname LIKE 'pg_temp%');
+ --Should work correctly to leaky-view scenario
+ CREATE TABLE lvtest1 (a int, b text);
+ CREATE TABLE lvtest2 (x int, y text);
+
+ INSERT INTO lvtest1 VALUES (10, 'aaa'), (11, 'bbb'), (12, 'ccc'), (13, 'ddd');
+ INSERT INTO lvtest2 VALUES (11, 'xxx'), (12, 'yyy'), (13, 'zzz'), (14, 'xyz');
+
+ CREATE OR REPLACE VIEW leaky_v1 WITH (security_barrier=true) AS
+ SELECT * FROM lvtest1 JOIN lvtest2 ON a = x WHERE a % 2 = 0; -- OK
+ CREATE OR REPLACE VIEW leaky_v2 WITH (security_barrier=false) AS
+ SELECT * FROM lvtest1 JOIN lvtest2 ON a = x WHERE a % 2 = 1; -- OK
+ CREATE OR REPLACE VIEW leaky_v3 WITH (invalid_option=1234) AS
+ SELECT * FROM lvtest1 JOIN lvtest2 ON a = x WHERE a % 2 = 1; -- Fail
+ SELECT relname, reloptions FROM pg_class WHERE relname like 'leaky_v%';
+
+ ALTER TABLE leaky_v1 RESET ( security_barrier );
+ ALTER TABLE leaky_v2 SET ( security_barrier );
+ SELECT relname, reloptions FROM pg_class WHERE relname like 'leaky_v%';
+
+ -- reloptions should be reset
+ CREATE OR REPLACE VIEW leaky_v2 AS
+ SELECT * FROM lvtest1 JOIN lvtest2 ON a = x WHERE a % 2 = 1;
+ SELECT relname, reloptions FROM pg_class WHERE relname like 'leaky_v%';
+
DROP SCHEMA temp_view_test CASCADE;
DROP SCHEMA testviewschm2 CASCADE;
2011/7/9 Robert Haas <robertmhaas@gmail.com>:
On Fri, Jul 8, 2011 at 4:57 PM, Noah Misch <noah@2ndquadrant.com> wrote:
Note that it does not matter whether we're actually doing an index scan -- a seq
scan with a filter using only leakproof operators is equally acceptable. What I
had in mind was to enumerate all operators in operator classes of indexes below
each security view. Those become the leak-free operators for that security
view. If the operator for an OpExpr is considered leak-free by all sources of
its operands, then we may push it down. That's purely a high-level sketch: I
haven't considered implementation concerns in any detail. The resulting
behavior could be surprising: adding an index may change a plan without the new
plan actually using the index.I lean toward favoring the pg_proc flag. Functions like "texteq" will be taken
as leakproof even if no involved table has an index on a text column. It works
for functions that will never take a place in an operator class, like
length(text). When a user reports a qualifier not getting pushed down, the
answer is much more satisfying: "Run 'CREATE OR REPLACE FUNCTION
... I_DONT_LEAK' as a superuser." Compare to "Define an operator class that
includes the function, if needed, and create an otherwise-useless index." The
main disadvantage I see is the loss of policy locality. Only a superuser (or
maybe database owner?) can create or modify declared-leakproof functions, and
that decision applies throughout the database. However, I think the other
advantages clearly outweigh that loss.This strikes me as a fairly compelling refutation of Heikki's proposed approach.
OK, I'll try to modify the patch according to the flag of pg_proc design.
As long as the default of user-defined function is off, and we provide
built-in functions
with appropriate configurations, it seems to me the burden of DBA is
quite limited.
Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
On Sat, Jul 09, 2011 at 09:00:30AM +0200, Kohei KaiGai wrote:
The attached patch is a revised version according to the approach that updates
pg_class system catalog before AlterTableInternal().
It invokes the new ResetViewOptions when rel->rd_options is not null, and it set
null on the pg_class.reloptions of the view and increments command counter.
+ /* + * ResetViewOptions + * + * It clears all the reloptions prior to replacing + */ + static void + ResetViewOptions(Oid viewOid) + { + Relation pg_class; + HeapTuple oldtup; + HeapTuple newtup; + Datum values[Natts_pg_class]; + bool nulls[Natts_pg_class]; + bool replaces[Natts_pg_class]; + + pg_class = heap_open(RelationRelationId, RowExclusiveLock); + + oldtup = SearchSysCache1(RELOID, DatumGetObjectId(viewOid));
Use SearchSysCacheCopy1, since you're modifying the tuple.
+ if (!HeapTupleIsValid(oldtup)) + elog(ERROR, "cache lookup failed for relation %u", viewOid); + + memset(values, 0, sizeof(values)); + memset(nulls, false, sizeof(nulls)); + memset(replaces, false, sizeof(replaces)); + + replaces[Anum_pg_class_reloptions - 1] = true; + nulls[Anum_pg_class_reloptions - 1] = true; + + newtup = heap_modify_tuple(oldtup, RelationGetDescr(pg_class), + values, nulls, replaces); + simple_heap_update(pg_class, &newtup->t_self, newtup); + + CatalogUpdateIndexes(pg_class, newtup); + + ReleaseSysCache(oldtup); + + heap_close(pg_class, RowExclusiveLock); + + CommandCounterIncrement();
Why is a CCI necessary?
+ }
In any event, we seem to be converging on a version of parts 0 and 1 that are
ready for committer. However, Robert contends that this will not be committed
separately from part 2. Unless someone wishes to contest that, I suggest we
mark this Returned with Feedback and let the CF entry for part 2 subsume its
future development. Does that sound reasonable?
Thanks,
nm
2011/7/9 Noah Misch <noah@2ndquadrant.com>:
On Sat, Jul 09, 2011 at 09:00:30AM +0200, Kohei KaiGai wrote:
The attached patch is a revised version according to the approach that updates
pg_class system catalog before AlterTableInternal().
It invokes the new ResetViewOptions when rel->rd_options is not null, and it set
null on the pg_class.reloptions of the view and increments command counter.+ /* + * ResetViewOptions + * + * It clears all the reloptions prior to replacing + */ + static void + ResetViewOptions(Oid viewOid) + { + Relation pg_class; + HeapTuple oldtup; + HeapTuple newtup; + Datum values[Natts_pg_class]; + bool nulls[Natts_pg_class]; + bool replaces[Natts_pg_class]; + + pg_class = heap_open(RelationRelationId, RowExclusiveLock); + + oldtup = SearchSysCache1(RELOID, DatumGetObjectId(viewOid));Use SearchSysCacheCopy1, since you're modifying the tuple.
The heap_modify_tuple() allocates a new tuple as a copy of old tuple.
No need to worry about.
+ if (!HeapTupleIsValid(oldtup)) + elog(ERROR, "cache lookup failed for relation %u", viewOid); + + memset(values, 0, sizeof(values)); + memset(nulls, false, sizeof(nulls)); + memset(replaces, false, sizeof(replaces)); + + replaces[Anum_pg_class_reloptions - 1] = true; + nulls[Anum_pg_class_reloptions - 1] = true; + + newtup = heap_modify_tuple(oldtup, RelationGetDescr(pg_class), + values, nulls, replaces); + simple_heap_update(pg_class, &newtup->t_self, newtup); + + CatalogUpdateIndexes(pg_class, newtup); + + ReleaseSysCache(oldtup); + + heap_close(pg_class, RowExclusiveLock); + + CommandCounterIncrement();Why is a CCI necessary?
ATExecSetRelOptions() reference the view to be updated using syscache,
however, this update will not become visible without CCI.
In the result, it will reference old tuple, then get an error because
it tries to
update already updated tuple.
+ }
In any event, we seem to be converging on a version of parts 0 and 1 that are
ready for committer. However, Robert contends that this will not be committed
separately from part 2. Unless someone wishes to contest that, I suggest we
mark this Returned with Feedback and let the CF entry for part 2 subsume its
future development. Does that sound reasonable?
At least, it seems to me we don't need to tackle to this matter from
the beginning
on the next commit fest again.
Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
On Sat, Jul 09, 2011 at 10:52:33AM +0200, Kohei KaiGai wrote:
2011/7/9 Noah Misch <noah@2ndquadrant.com>:
On Sat, Jul 09, 2011 at 09:00:30AM +0200, Kohei KaiGai wrote:
The attached patch is a revised version according to the approach that updates
pg_class system catalog before AlterTableInternal().
It invokes the new ResetViewOptions when rel->rd_options is not null, and it set
null on the pg_class.reloptions of the view and increments command counter.+ /* + �* ResetViewOptions + �* + �* It clears all the reloptions prior to replacing + �*/ + static void + ResetViewOptions(Oid viewOid) + { + � � Relation � � � �pg_class; + � � HeapTuple � � � oldtup; + � � HeapTuple � � � newtup; + � � Datum � � � � � values[Natts_pg_class]; + � � bool � � � � � �nulls[Natts_pg_class]; + � � bool � � � � � �replaces[Natts_pg_class]; + + � � pg_class = heap_open(RelationRelationId, RowExclusiveLock); + + � � oldtup = SearchSysCache1(RELOID, DatumGetObjectId(viewOid));Use SearchSysCacheCopy1, since you're modifying the tuple.
The heap_modify_tuple() allocates a new tuple as a copy of old tuple.
No need to worry about.
Ah, yes. Sorry for the noise.
+ � � if (!HeapTupleIsValid(oldtup)) + � � � � � � elog(ERROR, "cache lookup failed for relation %u", viewOid); + + � � memset(values, 0, sizeof(values)); + � � memset(nulls, false, sizeof(nulls)); + � � memset(replaces, false, sizeof(replaces)); + + � � replaces[Anum_pg_class_reloptions - 1] = true; + � � nulls[Anum_pg_class_reloptions - 1] = true; + + � � newtup = heap_modify_tuple(oldtup, RelationGetDescr(pg_class), + � � � � � � � � � � � � � � � � � � � � � � � � � � � �values, nulls, replaces); + � � simple_heap_update(pg_class, &newtup->t_self, newtup); + + � � CatalogUpdateIndexes(pg_class, newtup); + + � � ReleaseSysCache(oldtup); + + � � heap_close(pg_class, RowExclusiveLock); + + � � CommandCounterIncrement();Why is a CCI necessary?
ATExecSetRelOptions() reference the view to be updated using syscache,
however, this update will not become visible without CCI.
In the result, it will reference old tuple, then get an error because
it tries to
update already updated tuple.
Okay, thanks.
On 2011-07-09 09:14, Kohei KaiGai wrote:
OK, I'll try to modify the patch according to the flag of pg_proc design.
As long as the default of user-defined function is off, and we provide
built-in functions
with appropriate configurations, it seems to me the burden of DBA is
quite limited.
A different solution to the leaky view problem could be to check access
to a tuple at or near the heaptuple visibility level, in addition to
adding tuple access filter conditions to the query. This would have both
the possible performance benefits of the query rewriting solution, as
the everything is filtered before further processing at the heaptuple
visibility level. Fixing leaky views is not needed because they don't
exist in this case, the code is straightforward, and there's less change
of future security bugs by either misconfiguration of leakproof
functions or code that might introduce another leak path.
regards,
--
Yeb Havinga
http://www.mgrid.net/
Mastering Medical Data
2011/7/20 Yeb Havinga <yebhavinga@gmail.com>:
On 2011-07-09 09:14, Kohei KaiGai wrote:
OK, I'll try to modify the patch according to the flag of pg_proc design.
As long as the default of user-defined function is off, and we provide
built-in functions
with appropriate configurations, it seems to me the burden of DBA is
quite limited.A different solution to the leaky view problem could be to check access to a
tuple at or near the heaptuple visibility level, in addition to adding tuple
access filter conditions to the query. This would have both the possible
performance benefits of the query rewriting solution, as the everything is
filtered before further processing at the heaptuple visibility level. Fixing
leaky views is not needed because they don't exist in this case, the code is
straightforward, and there's less change of future security bugs by either
misconfiguration of leakproof functions or code that might introduce another
leak path.
I'm not fun with this approach. The harderst one to find out a solution is
a way to distinguish qualifiers of security policy and others.
Leaky functions looks like a harmless function, them the optimizer will
distribute them onto particular scan plans.
If it was executed on the visibility check of tuples, same problem will be
reproduced. So, I'm still fun with a flag of pg_proc catalog and idea of
security barrier.
Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
On Wed, Jul 20, 2011 at 09:02:59AM +0200, Yeb Havinga wrote:
On 2011-07-09 09:14, Kohei KaiGai wrote:
OK, I'll try to modify the patch according to the flag of pg_proc design.
As long as the default of user-defined function is off, and we provide
built-in functions
with appropriate configurations, it seems to me the burden of DBA is
quite limited.A different solution to the leaky view problem could be to check access
to a tuple at or near the heaptuple visibility level, in addition to
adding tuple access filter conditions to the query. This would have both
the possible performance benefits of the query rewriting solution, as
the everything is filtered before further processing at the heaptuple
visibility level. Fixing leaky views is not needed because they don't
exist in this case, the code is straightforward, and there's less change
of future security bugs by either misconfiguration of leakproof
functions or code that might introduce another leak path.
The SQL-level semantics of the view define the access rules in question. How
would you translate that into tests to apply at a lower level?
--
Noah Misch http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
On 2011-07-20 16:06, Noah Misch wrote:
The SQL-level semantics of the view define the access rules in question. How
would you translate that into tests to apply at a lower level?
I assumed the leaky view thread was about row level security, not about
access rules to views, since it was mentioned at the RLS wiki page for
se-pgsql. Sorry for the confusion.
regards,
Yeb
On 2011-07-20 16:15, Yeb Havinga wrote:
On 2011-07-20 16:06, Noah Misch wrote:
The SQL-level semantics of the view define the access rules in
question. How
would you translate that into tests to apply at a lower level?I assumed the leaky view thread was about row level security, not
about access rules to views, since it was mentioned at the RLS wiki
page for se-pgsql. Sorry for the confusion.
Had to digg a bit for the wiki, it was this one :
http://wiki.postgresql.org/wiki/RLS#Issue:_A_leaky_VIEWs_for_RLS
regards,
Yeb
On Wed, Jul 20, 2011 at 04:23:10PM +0200, Yeb Havinga wrote:
On 2011-07-20 16:15, Yeb Havinga wrote:
On 2011-07-20 16:06, Noah Misch wrote:
The SQL-level semantics of the view define the access rules in
question. How
would you translate that into tests to apply at a lower level?I assumed the leaky view thread was about row level security, not
about access rules to views, since it was mentioned at the RLS wiki
page for se-pgsql. Sorry for the confusion.Had to digg a bit for the wiki, it was this one :
http://wiki.postgresql.org/wiki/RLS#Issue:_A_leaky_VIEWs_for_RLS
It is about row-level security, broadly. These patches close the hazard
described in the latter half of this page:
http://www.postgresql.org/docs/9.0/static/rules-privileges.html
In the example given there, "phone NOT LIKE '412%'" is the (row-level) access
rule that needs to apply before any possibly-leaky function sees the tuple.
--
Noah Misch http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services