mirror of
https://github.com/postgres/postgres.git
synced 2025-07-14 08:21:07 +03:00
Improve RLS planning by marking individual quals with security levels.
In an RLS query, we must ensure that security filter quals are evaluated before ordinary query quals, in case the latter contain "leaky" functions that could expose the contents of sensitive rows. The original implementation of RLS planning ensured this by pushing the scan of a secured table into a sub-query that it marked as a security-barrier view. Unfortunately this results in very inefficient plans in many cases, because the sub-query cannot be flattened and gets planned independently of the rest of the query. To fix, drop the use of sub-queries to enforce RLS qual order, and instead mark each qual (RestrictInfo) with a security_level field establishing its priority for evaluation. Quals must be evaluated in security_level order, except that "leakproof" quals can be allowed to go ahead of quals of lower security_level, if it's helpful to do so. This has to be enforced within the ordering of any one list of quals to be evaluated at a table scan node, and we also have to ensure that quals are not chosen for early evaluation (i.e., use as an index qual or TID scan qual) if they're not allowed to go ahead of other quals at the scan node. This is sufficient to fix the problem for RLS quals, since we only support RLS policies on simple tables and thus RLS quals will always exist at the table scan level only. Eventually these qual ordering rules should be enforced for join quals as well, which would permit improving planning for explicit security-barrier views; but that's a task for another patch. Note that FDWs would need to be aware of these rules --- and not, for example, send an insecure qual for remote execution --- but since we do not yet allow RLS policies on foreign tables, the case doesn't arise. This will need to be addressed before we can allow such policies. Patch by me, reviewed by Stephen Frost and Dean Rasheed. Discussion: https://postgr.es/m/8185.1477432701@sss.pgh.pa.us
This commit is contained in:
@ -51,6 +51,9 @@ static List *deconstruct_recurse(PlannerInfo *root, Node *jtnode,
|
||||
bool below_outer_join,
|
||||
Relids *qualscope, Relids *inner_join_rels,
|
||||
List **postponed_qual_list);
|
||||
static void process_security_barrier_quals(PlannerInfo *root,
|
||||
int rti, Relids qualscope,
|
||||
bool below_outer_join);
|
||||
static SpecialJoinInfo *make_outerjoininfo(PlannerInfo *root,
|
||||
Relids left_rels, Relids right_rels,
|
||||
Relids inner_join_rels,
|
||||
@ -60,6 +63,7 @@ static void distribute_qual_to_rels(PlannerInfo *root, Node *clause,
|
||||
bool is_deduced,
|
||||
bool below_outer_join,
|
||||
JoinType jointype,
|
||||
Index security_level,
|
||||
Relids qualscope,
|
||||
Relids ojscope,
|
||||
Relids outerjoin_nonnullable,
|
||||
@ -745,8 +749,14 @@ deconstruct_recurse(PlannerInfo *root, Node *jtnode, bool below_outer_join,
|
||||
{
|
||||
int varno = ((RangeTblRef *) jtnode)->rtindex;
|
||||
|
||||
/* No quals to deal with, just return correct result */
|
||||
/* qualscope is just the one RTE */
|
||||
*qualscope = bms_make_singleton(varno);
|
||||
/* Deal with any securityQuals attached to the RTE */
|
||||
if (root->qual_security_level > 0)
|
||||
process_security_barrier_quals(root,
|
||||
varno,
|
||||
*qualscope,
|
||||
below_outer_join);
|
||||
/* A single baserel does not create an inner join */
|
||||
*inner_join_rels = NULL;
|
||||
joinlist = list_make1(jtnode);
|
||||
@ -810,6 +820,7 @@ deconstruct_recurse(PlannerInfo *root, Node *jtnode, bool below_outer_join,
|
||||
if (bms_is_subset(pq->relids, *qualscope))
|
||||
distribute_qual_to_rels(root, pq->qual,
|
||||
false, below_outer_join, JOIN_INNER,
|
||||
root->qual_security_level,
|
||||
*qualscope, NULL, NULL, NULL,
|
||||
NULL);
|
||||
else
|
||||
@ -825,6 +836,7 @@ deconstruct_recurse(PlannerInfo *root, Node *jtnode, bool below_outer_join,
|
||||
|
||||
distribute_qual_to_rels(root, qual,
|
||||
false, below_outer_join, JOIN_INNER,
|
||||
root->qual_security_level,
|
||||
*qualscope, NULL, NULL, NULL,
|
||||
postponed_qual_list);
|
||||
}
|
||||
@ -1002,6 +1014,7 @@ deconstruct_recurse(PlannerInfo *root, Node *jtnode, bool below_outer_join,
|
||||
|
||||
distribute_qual_to_rels(root, qual,
|
||||
false, below_outer_join, j->jointype,
|
||||
root->qual_security_level,
|
||||
*qualscope,
|
||||
ojscope, nonnullable_rels, NULL,
|
||||
postponed_qual_list);
|
||||
@ -1058,6 +1071,67 @@ deconstruct_recurse(PlannerInfo *root, Node *jtnode, bool below_outer_join,
|
||||
return joinlist;
|
||||
}
|
||||
|
||||
/*
|
||||
* process_security_barrier_quals
|
||||
* Transfer security-barrier quals into relation's baserestrictinfo list.
|
||||
*
|
||||
* The rewriter put any relevant security-barrier conditions into the RTE's
|
||||
* securityQuals field, but it's now time to copy them into the rel's
|
||||
* baserestrictinfo.
|
||||
*
|
||||
* In inheritance cases, we only consider quals attached to the parent rel
|
||||
* here; they will be valid for all children too, so it's okay to consider
|
||||
* them for purposes like equivalence class creation. Quals attached to
|
||||
* individual child rels will be dealt with during path creation.
|
||||
*/
|
||||
static void
|
||||
process_security_barrier_quals(PlannerInfo *root,
|
||||
int rti, Relids qualscope,
|
||||
bool below_outer_join)
|
||||
{
|
||||
RangeTblEntry *rte = root->simple_rte_array[rti];
|
||||
Index security_level = 0;
|
||||
ListCell *lc;
|
||||
|
||||
/*
|
||||
* Each element of the securityQuals list has been preprocessed into an
|
||||
* implicitly-ANDed list of clauses. All the clauses in a given sublist
|
||||
* should get the same security level, but successive sublists get higher
|
||||
* levels.
|
||||
*/
|
||||
foreach(lc, rte->securityQuals)
|
||||
{
|
||||
List *qualset = (List *) lfirst(lc);
|
||||
ListCell *lc2;
|
||||
|
||||
foreach(lc2, qualset)
|
||||
{
|
||||
Node *qual = (Node *) lfirst(lc2);
|
||||
|
||||
/*
|
||||
* We cheat to the extent of passing ojscope = qualscope rather
|
||||
* than its more logical value of NULL. The only effect this has
|
||||
* is to force a Var-free qual to be evaluated at the rel rather
|
||||
* than being pushed up to top of tree, which we don't want.
|
||||
*/
|
||||
distribute_qual_to_rels(root, qual,
|
||||
false,
|
||||
below_outer_join,
|
||||
JOIN_INNER,
|
||||
security_level,
|
||||
qualscope,
|
||||
qualscope,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL);
|
||||
}
|
||||
security_level++;
|
||||
}
|
||||
|
||||
/* Assert that qual_security_level is higher than anything we just used */
|
||||
Assert(security_level <= root->qual_security_level);
|
||||
}
|
||||
|
||||
/*
|
||||
* make_outerjoininfo
|
||||
* Build a SpecialJoinInfo for the current outer join
|
||||
@ -1516,6 +1590,7 @@ compute_semijoin_info(SpecialJoinInfo *sjinfo, List *clause)
|
||||
* 'below_outer_join': TRUE if the qual is from a JOIN/ON that is below the
|
||||
* nullable side of a higher-level outer join
|
||||
* 'jointype': type of join the qual is from (JOIN_INNER for a WHERE clause)
|
||||
* 'security_level': security_level to assign to the qual
|
||||
* 'qualscope': set of baserels the qual's syntactic scope covers
|
||||
* 'ojscope': NULL if not an outer-join qual, else the minimum set of baserels
|
||||
* needed to form this join
|
||||
@ -1545,6 +1620,7 @@ distribute_qual_to_rels(PlannerInfo *root, Node *clause,
|
||||
bool is_deduced,
|
||||
bool below_outer_join,
|
||||
JoinType jointype,
|
||||
Index security_level,
|
||||
Relids qualscope,
|
||||
Relids ojscope,
|
||||
Relids outerjoin_nonnullable,
|
||||
@ -1794,6 +1870,7 @@ distribute_qual_to_rels(PlannerInfo *root, Node *clause,
|
||||
is_pushed_down,
|
||||
outerjoin_delayed,
|
||||
pseudoconstant,
|
||||
security_level,
|
||||
relids,
|
||||
outerjoin_nonnullable,
|
||||
nullable_relids);
|
||||
@ -2142,6 +2219,9 @@ distribute_restrictinfo_to_rels(PlannerInfo *root,
|
||||
/* Add clause to rel's restriction list */
|
||||
rel->baserestrictinfo = lappend(rel->baserestrictinfo,
|
||||
restrictinfo);
|
||||
/* Update security level info */
|
||||
rel->baserestrict_min_security = Min(rel->baserestrict_min_security,
|
||||
restrictinfo->security_level);
|
||||
break;
|
||||
case BMS_MULTIPLE:
|
||||
|
||||
@ -2189,6 +2269,8 @@ distribute_restrictinfo_to_rels(PlannerInfo *root,
|
||||
* caller because this function is used after deconstruct_jointree, so we
|
||||
* don't have knowledge of where the clause items came from.)
|
||||
*
|
||||
* "security_level" is the security level to assign to the new restrictinfo.
|
||||
*
|
||||
* "both_const" indicates whether both items are known pseudo-constant;
|
||||
* in this case it is worth applying eval_const_expressions() in case we
|
||||
* can produce constant TRUE or constant FALSE. (Otherwise it's not,
|
||||
@ -2209,6 +2291,7 @@ process_implied_equality(PlannerInfo *root,
|
||||
Expr *item2,
|
||||
Relids qualscope,
|
||||
Relids nullable_relids,
|
||||
Index security_level,
|
||||
bool below_outer_join,
|
||||
bool both_const)
|
||||
{
|
||||
@ -2247,6 +2330,7 @@ process_implied_equality(PlannerInfo *root,
|
||||
*/
|
||||
distribute_qual_to_rels(root, (Node *) clause,
|
||||
true, below_outer_join, JOIN_INNER,
|
||||
security_level,
|
||||
qualscope, NULL, NULL, nullable_relids,
|
||||
NULL);
|
||||
}
|
||||
@ -2270,7 +2354,8 @@ build_implied_join_equality(Oid opno,
|
||||
Expr *item1,
|
||||
Expr *item2,
|
||||
Relids qualscope,
|
||||
Relids nullable_relids)
|
||||
Relids nullable_relids,
|
||||
Index security_level)
|
||||
{
|
||||
RestrictInfo *restrictinfo;
|
||||
Expr *clause;
|
||||
@ -2294,6 +2379,7 @@ build_implied_join_equality(Oid opno,
|
||||
true, /* is_pushed_down */
|
||||
false, /* outerjoin_delayed */
|
||||
false, /* pseudoconstant */
|
||||
security_level, /* security_level */
|
||||
qualscope, /* required_relids */
|
||||
NULL, /* outer_relids */
|
||||
nullable_relids); /* nullable_relids */
|
||||
|
Reference in New Issue
Block a user