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);
+ In addition, you might be able to leak contents of invisible tuples
+ using the following scenario:
+
+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();
+
+ This view also might seem secure, since any SELECT
+ from your_credit shall be rewritten into a
+ SELECT from the join of pg_authid>
+ and 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.
+
+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)
+
+ The reason is obvious from the result of EXPLAIN.
+
+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)
+
+ The supplied tricky only references
+ number and expire columns,
+ however, the qualifier to filter invisible tuples performs on
+ the scan of pg_authid.
+ Then, since the optimizer tries to minimize the number of tuples
+ being joined, the supplied qualifer got attached on the scan of
+ credit_cards.
+ In the result, it allows tricky to reference
+ contents of the credit_cards table.
+
+
+ The security_berrier 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
+ security_berrier=TRUE, 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.
+
+
+
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 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