mirror of
https://github.com/postgres/postgres.git
synced 2025-07-08 11:42:09 +03:00
Avoid invalidating all foreign-join cached plans when user mappings change.
We must not push down a foreign join when the foreign tables involved should be accessed under different user mappings. Previously we tried to enforce that rule literally during planning, but that meant that the resulting plans were dependent on the current contents of the pg_user_mapping catalog, and we had to blow away all cached plans containing any remote join when anything at all changed in pg_user_mapping. This could have been improved somewhat, but the fact that a syscache inval callback has very limited info about what changed made it hard to do better within that design. Instead, let's change the planner to not consider user mappings per se, but to allow a foreign join if both RTEs have the same checkAsUser value. If they do, then they necessarily will use the same user mapping at runtime, and we don't need to know specifically which one that is. Post-plan-time changes in pg_user_mapping no longer require any plan invalidation. This rule does give up some optimization ability, to wit where two foreign table references come from views with different owners or one's from a view and one's directly in the query, but nonetheless the same user mapping would have applied. We'll sacrifice the first case, but to not regress more than we have to in the second case, allow a foreign join involving both zero and nonzero checkAsUser values if the nonzero one is the same as the prevailing effective userID. In that case, mark the plan as only runnable by that userID. The plancache code already had a notion of plans being userID-specific, in order to support RLS. It was a little confused though, in particular lacking clarity of thought as to whether it was the rewritten query or just the finished plan that's dependent on the userID. Rearrange that code so that it's clearer what depends on which, and so that the same logic applies to both RLS-injected role dependency and foreign-join-injected role dependency. Note that this patch doesn't remove the other issue mentioned in the original complaint, which is that while we'll reliably stop using a foreign join if it's disallowed in a new context, we might fail to start using a foreign join if it's now allowed, but we previously created a generic cached plan that didn't use one. It was agreed that the chance of winning that way was not high enough to justify the much larger number of plan invalidations that would have to occur if we tried to cause it to happen. In passing, clean up randomly-varying spelling of EXPLAIN commands in postgres_fdw.sql, and fix a COSTS ON example that had been allowed to leak into the committed tests. This reverts most of commitsfbe5a3fb7
and5d4171d1c
, which were the previous attempt at ensuring we wouldn't push down foreign joins that span permissions contexts. Etsuro Fujita and Tom Lane Discussion: <d49c1e5b-f059-20f4-c132-e9752ee0113e@lab.ntt.co.jp>
This commit is contained in:
@ -213,8 +213,8 @@ add_paths_to_joinrel(PlannerInfo *root,
|
||||
|
||||
/*
|
||||
* 5. If inner and outer relations are foreign tables (or joins) belonging
|
||||
* to the same server and using the same user mapping, give the FDW a
|
||||
* chance to push down joins.
|
||||
* to the same server and assigned to the same user to check access
|
||||
* permissions as, give the FDW a chance to push down joins.
|
||||
*/
|
||||
if (joinrel->fdwroutine &&
|
||||
joinrel->fdwroutine->GetForeignJoinPaths)
|
||||
|
@ -3247,13 +3247,12 @@ create_foreignscan_plan(PlannerInfo *root, ForeignPath *best_path,
|
||||
scan_plan->fs_relids = best_path->path.parent->relids;
|
||||
|
||||
/*
|
||||
* If a join between foreign relations was pushed down, remember it. The
|
||||
* push-down safety of the join depends upon the server and user mapping
|
||||
* being same. That can change between planning and execution time, in
|
||||
* which case the plan should be invalidated.
|
||||
* If this is a foreign join, and to make it valid to push down we had to
|
||||
* assume that the current user is the same as some user explicitly named
|
||||
* in the query, mark the finished plan as depending on the current user.
|
||||
*/
|
||||
if (scan_relid == 0)
|
||||
root->glob->hasForeignJoin = true;
|
||||
if (rel->useridiscurrent)
|
||||
root->glob->dependsOnRole = true;
|
||||
|
||||
/*
|
||||
* Replace any outer-relation variables with nestloop params in the qual,
|
||||
|
@ -219,8 +219,7 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams)
|
||||
glob->lastRowMarkId = 0;
|
||||
glob->lastPlanNodeId = 0;
|
||||
glob->transientPlan = false;
|
||||
glob->hasRowSecurity = false;
|
||||
glob->hasForeignJoin = false;
|
||||
glob->dependsOnRole = false;
|
||||
|
||||
/*
|
||||
* Assess whether it's feasible to use parallel mode for this query. We
|
||||
@ -405,6 +404,8 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams)
|
||||
result->hasModifyingCTE = parse->hasModifyingCTE;
|
||||
result->canSetTag = parse->canSetTag;
|
||||
result->transientPlan = glob->transientPlan;
|
||||
result->dependsOnRole = glob->dependsOnRole;
|
||||
result->parallelModeNeeded = glob->parallelModeNeeded;
|
||||
result->planTree = top_plan;
|
||||
result->rtable = glob->finalrtable;
|
||||
result->resultRelations = glob->resultRelations;
|
||||
@ -415,9 +416,6 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams)
|
||||
result->relationOids = glob->relationOids;
|
||||
result->invalItems = glob->invalItems;
|
||||
result->nParamExec = glob->nParamExec;
|
||||
result->hasRowSecurity = glob->hasRowSecurity;
|
||||
result->parallelModeNeeded = glob->parallelModeNeeded;
|
||||
result->hasForeignJoin = glob->hasForeignJoin;
|
||||
|
||||
return result;
|
||||
}
|
||||
@ -1628,8 +1626,6 @@ grouping_planner(PlannerInfo *root, bool inheritance_update,
|
||||
* This may add new security barrier subquery RTEs to the rangetable.
|
||||
*/
|
||||
expand_security_quals(root, tlist);
|
||||
if (parse->hasRowSecurity)
|
||||
root->glob->hasRowSecurity = true;
|
||||
|
||||
/*
|
||||
* We are now done hacking up the query's targetlist. Most of the
|
||||
@ -1960,7 +1956,8 @@ grouping_planner(PlannerInfo *root, bool inheritance_update,
|
||||
* If the current_rel belongs to a single FDW, so does the final_rel.
|
||||
*/
|
||||
final_rel->serverid = current_rel->serverid;
|
||||
final_rel->umid = current_rel->umid;
|
||||
final_rel->userid = current_rel->userid;
|
||||
final_rel->useridiscurrent = current_rel->useridiscurrent;
|
||||
final_rel->fdwroutine = current_rel->fdwroutine;
|
||||
|
||||
/*
|
||||
@ -3337,7 +3334,8 @@ create_grouping_paths(PlannerInfo *root,
|
||||
* If the input rel belongs to a single FDW, so does the grouped rel.
|
||||
*/
|
||||
grouped_rel->serverid = input_rel->serverid;
|
||||
grouped_rel->umid = input_rel->umid;
|
||||
grouped_rel->userid = input_rel->userid;
|
||||
grouped_rel->useridiscurrent = input_rel->useridiscurrent;
|
||||
grouped_rel->fdwroutine = input_rel->fdwroutine;
|
||||
|
||||
/*
|
||||
@ -3891,7 +3889,8 @@ create_window_paths(PlannerInfo *root,
|
||||
* If the input rel belongs to a single FDW, so does the window rel.
|
||||
*/
|
||||
window_rel->serverid = input_rel->serverid;
|
||||
window_rel->umid = input_rel->umid;
|
||||
window_rel->userid = input_rel->userid;
|
||||
window_rel->useridiscurrent = input_rel->useridiscurrent;
|
||||
window_rel->fdwroutine = input_rel->fdwroutine;
|
||||
|
||||
/*
|
||||
@ -4071,7 +4070,8 @@ create_distinct_paths(PlannerInfo *root,
|
||||
* If the input rel belongs to a single FDW, so does the distinct_rel.
|
||||
*/
|
||||
distinct_rel->serverid = input_rel->serverid;
|
||||
distinct_rel->umid = input_rel->umid;
|
||||
distinct_rel->userid = input_rel->userid;
|
||||
distinct_rel->useridiscurrent = input_rel->useridiscurrent;
|
||||
distinct_rel->fdwroutine = input_rel->fdwroutine;
|
||||
|
||||
/* Estimate number of distinct rows there will be */
|
||||
@ -4279,7 +4279,8 @@ create_ordered_paths(PlannerInfo *root,
|
||||
* If the input rel belongs to a single FDW, so does the ordered_rel.
|
||||
*/
|
||||
ordered_rel->serverid = input_rel->serverid;
|
||||
ordered_rel->umid = input_rel->umid;
|
||||
ordered_rel->userid = input_rel->userid;
|
||||
ordered_rel->useridiscurrent = input_rel->useridiscurrent;
|
||||
ordered_rel->fdwroutine = input_rel->fdwroutine;
|
||||
|
||||
foreach(lc, input_rel->pathlist)
|
||||
|
@ -2432,9 +2432,10 @@ record_plan_function_dependency(PlannerInfo *root, Oid funcid)
|
||||
|
||||
/*
|
||||
* extract_query_dependencies
|
||||
* Given a not-yet-planned query or queries (i.e. a Query node or list
|
||||
* of Query nodes), extract dependencies just as set_plan_references
|
||||
* would do.
|
||||
* Given a rewritten, but not yet planned, query or queries
|
||||
* (i.e. a Query node or list of Query nodes), extract dependencies
|
||||
* just as set_plan_references would do. Also detect whether any
|
||||
* rewrite steps were affected by RLS.
|
||||
*
|
||||
* This is needed by plancache.c to handle invalidation of cached unplanned
|
||||
* queries.
|
||||
@ -2453,7 +2454,8 @@ extract_query_dependencies(Node *query,
|
||||
glob.type = T_PlannerGlobal;
|
||||
glob.relationOids = NIL;
|
||||
glob.invalItems = NIL;
|
||||
glob.hasRowSecurity = false;
|
||||
/* Hack: we use glob.dependsOnRole to collect hasRowSecurity flags */
|
||||
glob.dependsOnRole = false;
|
||||
|
||||
MemSet(&root, 0, sizeof(root));
|
||||
root.type = T_PlannerInfo;
|
||||
@ -2463,7 +2465,7 @@ extract_query_dependencies(Node *query,
|
||||
|
||||
*relationOids = glob.relationOids;
|
||||
*invalItems = glob.invalItems;
|
||||
*hasRowSecurity = glob.hasRowSecurity;
|
||||
*hasRowSecurity = glob.dependsOnRole;
|
||||
}
|
||||
|
||||
static bool
|
||||
@ -2479,10 +2481,6 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context)
|
||||
Query *query = (Query *) node;
|
||||
ListCell *lc;
|
||||
|
||||
/* Collect row security information */
|
||||
if (query->hasRowSecurity)
|
||||
context->glob->hasRowSecurity = true;
|
||||
|
||||
if (query->commandType == CMD_UTILITY)
|
||||
{
|
||||
/*
|
||||
@ -2494,6 +2492,10 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context)
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Remember if any Query has RLS quals applied by rewriter */
|
||||
if (query->hasRowSecurity)
|
||||
context->glob->dependsOnRole = true;
|
||||
|
||||
/* Collect relation OIDs in this Query's rtable */
|
||||
foreach(lc, query->rtable)
|
||||
{
|
||||
|
@ -15,8 +15,6 @@
|
||||
#include "postgres.h"
|
||||
|
||||
#include "miscadmin.h"
|
||||
#include "catalog/pg_class.h"
|
||||
#include "foreign/foreign.h"
|
||||
#include "optimizer/clauses.h"
|
||||
#include "optimizer/cost.h"
|
||||
#include "optimizer/pathnode.h"
|
||||
@ -107,7 +105,6 @@ build_simple_rel(PlannerInfo *root, int relid, RelOptKind reloptkind)
|
||||
rel->consider_startup = (root->tuple_fraction > 0);
|
||||
rel->consider_param_startup = false; /* might get changed later */
|
||||
rel->consider_parallel = false; /* might get changed later */
|
||||
rel->rel_parallel_workers = -1; /* set up in GetRelationInfo */
|
||||
rel->reltarget = create_empty_pathtarget();
|
||||
rel->pathlist = NIL;
|
||||
rel->ppilist = NIL;
|
||||
@ -129,8 +126,10 @@ build_simple_rel(PlannerInfo *root, int relid, RelOptKind reloptkind)
|
||||
rel->allvisfrac = 0;
|
||||
rel->subroot = NULL;
|
||||
rel->subplan_params = NIL;
|
||||
rel->rel_parallel_workers = -1; /* set up in GetRelationInfo */
|
||||
rel->serverid = InvalidOid;
|
||||
rel->umid = InvalidOid;
|
||||
rel->userid = rte->checkAsUser;
|
||||
rel->useridiscurrent = false;
|
||||
rel->fdwroutine = NULL;
|
||||
rel->fdw_private = NULL;
|
||||
rel->baserestrictinfo = NIL;
|
||||
@ -170,30 +169,6 @@ build_simple_rel(PlannerInfo *root, int relid, RelOptKind reloptkind)
|
||||
break;
|
||||
}
|
||||
|
||||
/* For foreign tables get the user mapping */
|
||||
if (rte->relkind == RELKIND_FOREIGN_TABLE)
|
||||
{
|
||||
/*
|
||||
* This should match what ExecCheckRTEPerms() does.
|
||||
*
|
||||
* Note that if the plan ends up depending on the user OID in any way
|
||||
* - e.g. if it depends on the computed user mapping OID - we must
|
||||
* ensure that it gets invalidated in the case of a user OID change.
|
||||
* See RevalidateCachedQuery and more generally the hasForeignJoin
|
||||
* flags in PlannerGlobal and PlannedStmt.
|
||||
*
|
||||
* It's possible, and not necessarily an error, for rel->umid to be
|
||||
* InvalidOid even though rel->serverid is set. That just means there
|
||||
* is a server with no user mapping.
|
||||
*/
|
||||
Oid userid;
|
||||
|
||||
userid = OidIsValid(rte->checkAsUser) ? rte->checkAsUser : GetUserId();
|
||||
rel->umid = GetUserMappingId(userid, rel->serverid, true);
|
||||
}
|
||||
else
|
||||
rel->umid = InvalidOid;
|
||||
|
||||
/* Save the finished struct in the query's simple_rel_array */
|
||||
root->simple_rel_array[relid] = rel;
|
||||
|
||||
@ -423,8 +398,10 @@ build_join_rel(PlannerInfo *root,
|
||||
joinrel->allvisfrac = 0;
|
||||
joinrel->subroot = NULL;
|
||||
joinrel->subplan_params = NIL;
|
||||
joinrel->rel_parallel_workers = -1;
|
||||
joinrel->serverid = InvalidOid;
|
||||
joinrel->umid = InvalidOid;
|
||||
joinrel->userid = InvalidOid;
|
||||
joinrel->useridiscurrent = false;
|
||||
joinrel->fdwroutine = NULL;
|
||||
joinrel->fdw_private = NULL;
|
||||
joinrel->baserestrictinfo = NIL;
|
||||
@ -435,24 +412,43 @@ build_join_rel(PlannerInfo *root,
|
||||
|
||||
/*
|
||||
* Set up foreign-join fields if outer and inner relation are foreign
|
||||
* tables (or joins) belonging to the same server and using the same user
|
||||
* mapping.
|
||||
* tables (or joins) belonging to the same server and assigned to the same
|
||||
* user to check access permissions as. In addition to an exact match of
|
||||
* userid, we allow the case where one side has zero userid (implying
|
||||
* current user) and the other side has explicit userid that happens to
|
||||
* equal the current user; but in that case, pushdown of the join is only
|
||||
* valid for the current user. The useridiscurrent field records whether
|
||||
* we had to make such an assumption for this join or any sub-join.
|
||||
*
|
||||
* Otherwise those fields are left invalid, so FDW API will not be called
|
||||
* for the join relation.
|
||||
*
|
||||
* For FDWs like file_fdw, which ignore user mapping, the user mapping id
|
||||
* associated with the joining relation may be invalid. A valid serverid
|
||||
* distinguishes between a pushed down join with no user mapping and a
|
||||
* join which can not be pushed down because of user mapping mismatch.
|
||||
* Otherwise these fields are left invalid, so GetForeignJoinPaths will
|
||||
* not be called for the join relation.
|
||||
*/
|
||||
if (OidIsValid(outer_rel->serverid) &&
|
||||
inner_rel->serverid == outer_rel->serverid &&
|
||||
inner_rel->umid == outer_rel->umid)
|
||||
inner_rel->serverid == outer_rel->serverid)
|
||||
{
|
||||
joinrel->serverid = outer_rel->serverid;
|
||||
joinrel->umid = outer_rel->umid;
|
||||
joinrel->fdwroutine = outer_rel->fdwroutine;
|
||||
if (inner_rel->userid == outer_rel->userid)
|
||||
{
|
||||
joinrel->serverid = outer_rel->serverid;
|
||||
joinrel->userid = outer_rel->userid;
|
||||
joinrel->useridiscurrent = outer_rel->useridiscurrent || inner_rel->useridiscurrent;
|
||||
joinrel->fdwroutine = outer_rel->fdwroutine;
|
||||
}
|
||||
else if (!OidIsValid(inner_rel->userid) &&
|
||||
outer_rel->userid == GetUserId())
|
||||
{
|
||||
joinrel->serverid = outer_rel->serverid;
|
||||
joinrel->userid = outer_rel->userid;
|
||||
joinrel->useridiscurrent = true;
|
||||
joinrel->fdwroutine = outer_rel->fdwroutine;
|
||||
}
|
||||
else if (!OidIsValid(outer_rel->userid) &&
|
||||
inner_rel->userid == GetUserId())
|
||||
{
|
||||
joinrel->serverid = outer_rel->serverid;
|
||||
joinrel->userid = inner_rel->userid;
|
||||
joinrel->useridiscurrent = true;
|
||||
joinrel->fdwroutine = outer_rel->fdwroutine;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
Reference in New Issue
Block a user