uint64 numberTuples,
ScanDirection direction,
DestReceiver *dest);
-static bool ExecCheckRTEPerms(RangeTblEntry *rte);
static bool ExecCheckRTEPermsModified(Oid relOid, Oid userid,
Bitmapset *modifiedCols,
AclMode requiredPerms);
* ExecCheckRTEPerms
* Check access permissions for a single RTE.
*/
-static bool
+bool
ExecCheckRTEPerms(RangeTblEntry *rte)
{
AclMode requiredPerms;
#include "partitioning/partdesc.h"
#include "rewrite/rewriteManip.h"
#include "storage/dsm_impl.h"
+#include "utils/acl.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
#include "utils/selfuncs.h"
bms_make_singleton(parse->resultRelation);
}
+ /*
+ * This would be a convenient time to check access permissions for all
+ * relations mentioned in the query, since it would be better to fail now,
+ * before doing any detailed planning. However, for historical reasons,
+ * we leave this to be done at executor startup.
+ *
+ * Note, however, that we do need to check access permissions for any view
+ * relations mentioned in the query, in order to prevent information being
+ * leaked by selectivity estimation functions, which only check view owner
+ * permissions on underlying tables (see all_rows_selectable() and its
+ * callers). This is a little ugly, because it means that access
+ * permissions for views will be checked twice, which is another reason
+ * why it would be better to do all the ACL checks here.
+ */
+ foreach(l, parse->rtable)
+ {
+ RangeTblEntry *rte = lfirst_node(RangeTblEntry, l);
+
+ if (rte->relkind == RELKIND_VIEW)
+ {
+ bool result;
+
+ result = ExecCheckRTEPerms(rte);
+ if (!result)
+ aclcheck_error(ACLCHECK_NO_PRIV, OBJECT_VIEW,
+ get_rel_name(rte->relid));
+ }
+ }
+
/*
* Preprocess RowMark information. We need to do this after subquery
* pullup, so that all base relations are present.
* so we can't cope with system columns.
* *exprs: input/output parameter collecting primitive subclauses within
* the clause tree
+ * *leakproof: input/output parameter recording the leakproofness of the
+ * clause tree. This should be true initially, and will be set to false
+ * if any operator function used in an OpExpr is not leakproof.
*
* Returns false if there is something we definitively can't handle.
* On true return, we can proceed to match the *exprs against statistics.
static bool
statext_is_compatible_clause_internal(PlannerInfo *root, Node *clause,
Index relid, Bitmapset **attnums,
- List **exprs)
+ List **exprs, bool *leakproof)
{
/* Look inside any binary-compatible relabeling (as in examine_variable) */
if (IsA(clause, RelabelType))
/* (Var/Expr op Const) or (Const op Var/Expr) */
if (is_opclause(clause))
{
- RangeTblEntry *rte = root->simple_rte_array[relid];
OpExpr *expr = (OpExpr *) clause;
Node *clause_expr;
return false;
}
- /*
- * If there are any securityQuals on the RTE from security barrier
- * views or RLS policies, then the user may not have access to all the
- * table's data, and we must check that the operator is leak-proof.
- *
- * If the operator is leaky, then we must ignore this clause for the
- * purposes of estimating with MCV lists, otherwise the operator might
- * reveal values from the MCV list that the user doesn't have
- * permission to see.
- */
- if (rte->securityQuals != NIL &&
- !get_func_leakproof(get_opcode(expr->opno)))
- return false;
+ /* Check if the operator is leakproof */
+ if (*leakproof)
+ *leakproof = get_func_leakproof(get_opcode(expr->opno));
/* Check (Var op Const) or (Const op Var) clauses by recursing. */
if (IsA(clause_expr, Var))
return statext_is_compatible_clause_internal(root, clause_expr,
- relid, attnums, exprs);
+ relid, attnums,
+ exprs, leakproof);
/* Otherwise we have (Expr op Const) or (Const op Expr). */
*exprs = lappend(*exprs, clause_expr);
/* Var/Expr IN Array */
if (IsA(clause, ScalarArrayOpExpr))
{
- RangeTblEntry *rte = root->simple_rte_array[relid];
ScalarArrayOpExpr *expr = (ScalarArrayOpExpr *) clause;
Node *clause_expr;
bool expronleft;
return false;
}
- /*
- * If there are any securityQuals on the RTE from security barrier
- * views or RLS policies, then the user may not have access to all the
- * table's data, and we must check that the operator is leak-proof.
- *
- * If the operator is leaky, then we must ignore this clause for the
- * purposes of estimating with MCV lists, otherwise the operator might
- * reveal values from the MCV list that the user doesn't have
- * permission to see.
- */
- if (rte->securityQuals != NIL &&
- !get_func_leakproof(get_opcode(expr->opno)))
- return false;
+ /* Check if the operator is leakproof */
+ if (*leakproof)
+ *leakproof = get_func_leakproof(get_opcode(expr->opno));
/* Check Var IN Array clauses by recursing. */
if (IsA(clause_expr, Var))
return statext_is_compatible_clause_internal(root, clause_expr,
- relid, attnums, exprs);
+ relid, attnums,
+ exprs, leakproof);
/* Otherwise we have Expr IN Array. */
*exprs = lappend(*exprs, clause_expr);
*/
if (!statext_is_compatible_clause_internal(root,
(Node *) lfirst(lc),
- relid, attnums, exprs))
+ relid, attnums, exprs,
+ leakproof))
return false;
}
/* Check Var IS NULL clauses by recursing. */
if (IsA(nt->arg, Var))
- return statext_is_compatible_clause_internal(root, (Node *) (nt->arg),
- relid, attnums, exprs);
+ return statext_is_compatible_clause_internal(root,
+ (Node *) (nt->arg),
+ relid, attnums,
+ exprs, leakproof);
/* Otherwise we have Expr IS NULL. */
*exprs = lappend(*exprs, nt->arg);
statext_is_compatible_clause(PlannerInfo *root, Node *clause, Index relid,
Bitmapset **attnums, List **exprs)
{
- RangeTblEntry *rte = root->simple_rte_array[relid];
RestrictInfo *rinfo;
int clause_relid;
- Oid userid;
+ bool leakproof;
/*
* Special-case handling for bare BoolExpr AND clauses, because the
clause_relid != relid)
return false;
- /* Check the clause and determine what attributes it references. */
+ /*
+ * Check the clause, determine what attributes it references, and whether
+ * it includes any non-leakproof operators.
+ */
+ leakproof = true;
if (!statext_is_compatible_clause_internal(root, (Node *) rinfo->clause,
- relid, attnums, exprs))
+ relid, attnums, exprs,
+ &leakproof))
return false;
/*
- * Check that the user has permission to read all required attributes. Use
- * checkAsUser if it's set, in case we're accessing the table via a view.
+ * If the clause includes any non-leakproof operators, check that the user
+ * has permission to read all required attributes, otherwise the operators
+ * might reveal values from the MCV list that the user doesn't have
+ * permission to see. We require all rows to be selectable --- there must
+ * be no securityQuals from security barrier views or RLS policies. See
+ * similar code in examine_variable(), examine_simple_variable(), and
+ * statistic_proc_security_check().
+ *
+ * Note that for an inheritance child, the permission checks are performed
+ * on the inheritance root parent, and whole-table select privilege on the
+ * parent doesn't guarantee that the user could read all columns of the
+ * child. Therefore we must check all referenced columns.
*/
- userid = rte->checkAsUser ? rte->checkAsUser : GetUserId();
-
- /* Table-level SELECT privilege is sufficient for all columns */
- if (pg_class_aclcheck(rte->relid, userid, ACL_SELECT) != ACLCHECK_OK)
+ if (!leakproof)
{
Bitmapset *clause_attnums = NULL;
int attnum = -1;
if (*exprs != NIL)
pull_varattnos((Node *) *exprs, relid, &clause_attnums);
- attnum = -1;
- while ((attnum = bms_next_member(clause_attnums, attnum)) >= 0)
- {
- /* Undo the offset */
- AttrNumber attno = attnum + FirstLowInvalidHeapAttributeNumber;
-
- if (attno == InvalidAttrNumber)
- {
- /* Whole-row reference, so must have access to all columns */
- if (pg_attribute_aclcheck_all(rte->relid, userid, ACL_SELECT,
- ACLMASK_ALL) != ACLCHECK_OK)
- return false;
- }
- else
- {
- if (pg_attribute_aclcheck(rte->relid, attno, userid,
- ACL_SELECT) != ACLCHECK_OK)
- return false;
- }
- }
+ /* Must have permission to read all rows from these columns */
+ if (!all_rows_selectable(root, relid, clause_attnums))
+ return false;
}
/* If we reach here, the clause is OK */
* this query. (Caution: this should be trusted for statistical
* purposes only, since we do not check indimmediate nor verify that
* the exact same definition of equality applies.)
- * acl_ok: true if current user has permission to read the column(s)
- * underlying the pg_statistic entry. This is consulted by
+ * acl_ok: true if current user has permission to read all table rows from
+ * the column(s) underlying the pg_statistic entry. This is consulted by
* statistic_proc_security_check().
*
* Caller is responsible for doing ReleaseVariableStats() before exiting.
if (HeapTupleIsValid(vardata->statsTuple))
{
- /* Get index's table for permission check */
- RangeTblEntry *rte;
- Oid userid;
-
- rte = planner_rt_fetch(index->rel->relid, root);
- Assert(rte->rtekind == RTE_RELATION);
-
- /*
- * Use checkAsUser if it's set, in case we're
- * accessing the table via a view.
- */
- userid = rte->checkAsUser ? rte->checkAsUser : GetUserId();
-
/*
+ * Test if user has permission to access all
+ * rows from the index's table.
+ *
* For simplicity, we insist on the whole
* table being selectable, rather than trying
* to identify which column(s) the index
- * depends on. Also require all rows to be
- * selectable --- there must be no
- * securityQuals from security barrier views
- * or RLS policies.
+ * depends on.
+ *
+ * Note that for an inheritance child,
+ * permissions are checked on the inheritance
+ * root parent, and whole-table select
+ * privilege on the parent doesn't quite
+ * guarantee that the user could read all
+ * columns of the child. But in practice it's
+ * unlikely that any interesting security
+ * violation could result from allowing access
+ * to the expression index's stats, so we
+ * allow it anyway. See similar code in
+ * examine_simple_variable() for additional
+ * comments.
*/
vardata->acl_ok =
- rte->securityQuals == NIL &&
- (pg_class_aclcheck(rte->relid, userid,
- ACL_SELECT) == ACLCHECK_OK);
-
- /*
- * If the user doesn't have permissions to
- * access an inheritance child relation, check
- * the permissions of the table actually
- * mentioned in the query, since most likely
- * the user does have that permission. Note
- * that whole-table select privilege on the
- * parent doesn't quite guarantee that the
- * user could read all columns of the child.
- * But in practice it's unlikely that any
- * interesting security violation could result
- * from allowing access to the expression
- * index's stats, so we allow it anyway. See
- * similar code in examine_simple_variable()
- * for additional comments.
- */
- if (!vardata->acl_ok &&
- root->append_rel_array != NULL)
- {
- AppendRelInfo *appinfo;
- Index varno = index->rel->relid;
-
- appinfo = root->append_rel_array[varno];
- while (appinfo &&
- planner_rt_fetch(appinfo->parent_relid,
- root)->rtekind == RTE_RELATION)
- {
- varno = appinfo->parent_relid;
- appinfo = root->append_rel_array[varno];
- }
- if (varno != index->rel->relid)
- {
- /* Repeat access check on this rel */
- rte = planner_rt_fetch(varno, root);
- Assert(rte->rtekind == RTE_RELATION);
-
- userid = rte->checkAsUser ? rte->checkAsUser : GetUserId();
-
- vardata->acl_ok =
- rte->securityQuals == NIL &&
- (pg_class_aclcheck(rte->relid,
- userid,
- ACL_SELECT) == ACLCHECK_OK);
- }
- }
+ all_rows_selectable(root,
+ index->rel->relid,
+ NULL);
}
else
{
{
HeapTuple t = statext_expressions_load(info->statOid, pos);
- /* Get statistics object's table for permission check */
- RangeTblEntry *rte;
- Oid userid;
-
vardata->statsTuple = t;
/*
*/
vardata->freefunc = ReleaseDummy;
- rte = planner_rt_fetch(onerel->relid, root);
- Assert(rte->rtekind == RTE_RELATION);
-
- /*
- * Use checkAsUser if it's set, in case we're accessing
- * the table via a view.
- */
- userid = rte->checkAsUser ? rte->checkAsUser : GetUserId();
-
/*
+ * Test if user has permission to access all rows from the
+ * table.
+ *
* For simplicity, we insist on the whole table being
* selectable, rather than trying to identify which
- * column(s) the statistics object depends on. Also
- * require all rows to be selectable --- there must be no
- * securityQuals from security barrier views or RLS
- * policies.
- */
- vardata->acl_ok =
- rte->securityQuals == NIL &&
- (pg_class_aclcheck(rte->relid, userid,
- ACL_SELECT) == ACLCHECK_OK);
-
- /*
- * If the user doesn't have permissions to access an
- * inheritance child relation, check the permissions of
- * the table actually mentioned in the query, since most
- * likely the user does have that permission. Note that
- * whole-table select privilege on the parent doesn't
- * quite guarantee that the user could read all columns of
- * the child. But in practice it's unlikely that any
- * interesting security violation could result from
- * allowing access to the expression stats, so we allow it
- * anyway. See similar code in examine_simple_variable()
- * for additional comments.
+ * column(s) the statistics object depends on.
+ *
+ * Note that for an inheritance child, permissions are
+ * checked on the inheritance root parent, and whole-table
+ * select privilege on the parent doesn't quite guarantee
+ * that the user could read all columns of the child. But
+ * in practice it's unlikely that any interesting security
+ * violation could result from allowing access to the
+ * expression stats, so we allow it anyway. See similar
+ * code in examine_simple_variable() for additional
+ * comments.
*/
- if (!vardata->acl_ok &&
- root->append_rel_array != NULL)
- {
- AppendRelInfo *appinfo;
- Index varno = onerel->relid;
-
- appinfo = root->append_rel_array[varno];
- while (appinfo &&
- planner_rt_fetch(appinfo->parent_relid,
- root)->rtekind == RTE_RELATION)
- {
- varno = appinfo->parent_relid;
- appinfo = root->append_rel_array[varno];
- }
- if (varno != onerel->relid)
- {
- /* Repeat access check on this rel */
- rte = planner_rt_fetch(varno, root);
- Assert(rte->rtekind == RTE_RELATION);
-
- userid = rte->checkAsUser ? rte->checkAsUser : GetUserId();
-
- vardata->acl_ok =
- rte->securityQuals == NIL &&
- (pg_class_aclcheck(rte->relid,
- userid,
- ACL_SELECT) == ACLCHECK_OK);
- }
- }
+ vardata->acl_ok = all_rows_selectable(root,
+ onerel->relid,
+ NULL);
break;
}
if (HeapTupleIsValid(vardata->statsTuple))
{
- Oid userid;
-
/*
- * Check if user has permission to read this column. We require
- * all rows to be accessible, so there must be no securityQuals
- * from security barrier views or RLS policies. Use checkAsUser
- * if it's set, in case we're accessing the table via a view.
+ * Test if user has permission to read all rows from this column.
+ *
+ * This requires that the user has the appropriate SELECT
+ * privileges and that there are no securityQuals from security
+ * barrier views or RLS policies. If that's not the case, then we
+ * only permit leakproof functions to be passed pg_statistic data
+ * in vardata, otherwise the functions might reveal data that the
+ * user doesn't have permission to see --- see
+ * statistic_proc_security_check().
*/
- userid = rte->checkAsUser ? rte->checkAsUser : GetUserId();
-
vardata->acl_ok =
- rte->securityQuals == NIL &&
- ((pg_class_aclcheck(rte->relid, userid,
- ACL_SELECT) == ACLCHECK_OK) ||
- (pg_attribute_aclcheck(rte->relid, var->varattno, userid,
- ACL_SELECT) == ACLCHECK_OK));
-
- /*
- * If the user doesn't have permissions to access an inheritance
- * child relation or specifically this attribute, check the
- * permissions of the table/column actually mentioned in the
- * query, since most likely the user does have that permission
- * (else the query will fail at runtime), and if the user can read
- * the column there then he can get the values of the child table
- * too. To do that, we must find out which of the root parent's
- * attributes the child relation's attribute corresponds to.
- */
- if (!vardata->acl_ok && var->varattno > 0 &&
- root->append_rel_array != NULL)
- {
- AppendRelInfo *appinfo;
- Index varno = var->varno;
- int varattno = var->varattno;
- bool found = false;
-
- appinfo = root->append_rel_array[varno];
-
- /*
- * Partitions are mapped to their immediate parent, not the
- * root parent, so must be ready to walk up multiple
- * AppendRelInfos. But stop if we hit a parent that is not
- * RTE_RELATION --- that's a flattened UNION ALL subquery, not
- * an inheritance parent.
- */
- while (appinfo &&
- planner_rt_fetch(appinfo->parent_relid,
- root)->rtekind == RTE_RELATION)
- {
- int parent_varattno;
-
- found = false;
- if (varattno <= 0 || varattno > appinfo->num_child_cols)
- break; /* safety check */
- parent_varattno = appinfo->parent_colnos[varattno - 1];
- if (parent_varattno == 0)
- break; /* Var is local to child */
-
- varno = appinfo->parent_relid;
- varattno = parent_varattno;
- found = true;
-
- /* If the parent is itself a child, continue up. */
- appinfo = root->append_rel_array[varno];
- }
-
- /*
- * In rare cases, the Var may be local to the child table, in
- * which case, we've got to live with having no access to this
- * column's stats.
- */
- if (!found)
- return;
-
- /* Repeat the access check on this parent rel & column */
- rte = planner_rt_fetch(varno, root);
- Assert(rte->rtekind == RTE_RELATION);
-
- userid = rte->checkAsUser ? rte->checkAsUser : GetUserId();
-
- vardata->acl_ok =
- rte->securityQuals == NIL &&
- ((pg_class_aclcheck(rte->relid, userid,
- ACL_SELECT) == ACLCHECK_OK) ||
- (pg_attribute_aclcheck(rte->relid, varattno, userid,
- ACL_SELECT) == ACLCHECK_OK));
- }
+ all_rows_selectable(root, var->varno,
+ bms_make_singleton(var->varattno - FirstLowInvalidHeapAttributeNumber));
}
else
{
}
}
+/*
+ * all_rows_selectable
+ * Test whether the user has permission to select all rows from a given
+ * relation.
+ *
+ * Inputs:
+ * root: the planner info
+ * varno: the index of the relation (assumed to be an RTE_RELATION)
+ * varattnos: the attributes for which permission is required, or NULL if
+ * whole-table access is required
+ *
+ * Returns true if the user has the required select permissions, and there are
+ * no securityQuals from security barrier views or RLS policies.
+ *
+ * Note that if the relation is an inheritance child relation, securityQuals
+ * and access permissions are checked against the inheritance root parent (the
+ * relation actually mentioned in the query) --- see the comments in
+ * expand_single_inheritance_child() for an explanation of why it has to be
+ * done this way.
+ *
+ * If varattnos is non-NULL, its attribute numbers should be offset by
+ * FirstLowInvalidHeapAttributeNumber so that system attributes can be
+ * checked. If varattnos is NULL, only table-level SELECT privileges are
+ * checked, not any column-level privileges.
+ *
+ * Note: if the relation is accessed via a view, this function actually tests
+ * whether the view owner has permission to select from the relation. To
+ * ensure that the current user has permission, it is also necessary to check
+ * that the current user has permission to select from the view, which we do
+ * at planner-startup --- see subquery_planner().
+ *
+ * This is exported so that other estimation functions can use it.
+ */
+bool
+all_rows_selectable(PlannerInfo *root, Index varno, Bitmapset *varattnos)
+{
+ RangeTblEntry *rte = planner_rt_fetch(varno, root);
+ int varattno;
+ Oid userid;
+
+ Assert(rte->rtekind == RTE_RELATION);
+
+ /*
+ * Permissions and securityQuals must be checked on the table actually
+ * mentioned in the query, so if this is an inheritance child, navigate up
+ * to the inheritance root parent. If the user can read the whole table
+ * or the required columns there, then they can read from the child table
+ * too. For per-column checks, we must find out which of the root
+ * parent's attributes the child relation's attributes correspond to.
+ */
+ if (root->append_rel_array != NULL)
+ {
+ AppendRelInfo *appinfo;
+
+ appinfo = root->append_rel_array[varno];
+
+ /*
+ * Partitions are mapped to their immediate parent, not the root
+ * parent, so must be ready to walk up multiple AppendRelInfos. But
+ * stop if we hit a parent that is not RTE_RELATION --- that's a
+ * flattened UNION ALL subquery, not an inheritance parent.
+ */
+ while (appinfo &&
+ planner_rt_fetch(appinfo->parent_relid,
+ root)->rtekind == RTE_RELATION)
+ {
+ Bitmapset *parent_varattnos = NULL;
+
+ /*
+ * For each child attribute, find the corresponding parent
+ * attribute. In rare cases, the attribute may be local to the
+ * child table, in which case, we've got to live with having no
+ * access to this column.
+ */
+ varattno = -1;
+ while ((varattno = bms_next_member(varattnos, varattno)) >= 0)
+ {
+ AttrNumber attno;
+ AttrNumber parent_attno;
+
+ attno = varattno + FirstLowInvalidHeapAttributeNumber;
+
+ if (attno == InvalidAttrNumber)
+ {
+ /*
+ * Whole-row reference, so must map each column of the
+ * child to the parent table.
+ */
+ for (attno = 1; attno <= appinfo->num_child_cols; attno++)
+ {
+ parent_attno = appinfo->parent_colnos[attno - 1];
+ if (parent_attno == 0)
+ return false; /* attr is local to child */
+ parent_varattnos =
+ bms_add_member(parent_varattnos,
+ parent_attno - FirstLowInvalidHeapAttributeNumber);
+ }
+ }
+ else
+ {
+ if (attno < 0)
+ {
+ /* System attnos are the same in all tables */
+ parent_attno = attno;
+ }
+ else
+ {
+ if (attno > appinfo->num_child_cols)
+ return false; /* safety check */
+ parent_attno = appinfo->parent_colnos[attno - 1];
+ if (parent_attno == 0)
+ return false; /* attr is local to child */
+ }
+ parent_varattnos =
+ bms_add_member(parent_varattnos,
+ parent_attno - FirstLowInvalidHeapAttributeNumber);
+ }
+ }
+
+ /* If the parent is itself a child, continue up */
+ varno = appinfo->parent_relid;
+ varattnos = parent_varattnos;
+ appinfo = root->append_rel_array[varno];
+ }
+
+ /* Perform the access check on this parent rel */
+ rte = planner_rt_fetch(varno, root);
+ Assert(rte->rtekind == RTE_RELATION);
+ }
+
+ /*
+ * For all rows to be accessible, there must be no securityQuals from
+ * security barrier views or RLS policies.
+ */
+ if (rte->securityQuals != NIL)
+ return false;
+
+ /*
+ * Use checkAsUser for privilege checks if it's set, in case we're
+ * accessing the table via a view.
+ */
+ userid = rte->checkAsUser ? rte->checkAsUser : GetUserId();
+
+ /*
+ * Test for table-level SELECT privilege.
+ *
+ * If varattnos is non-NULL, this is sufficient to give access to all
+ * requested attributes, even for a child table, since we have verified
+ * that all required child columns have matching parent columns.
+ *
+ * If varattnos is NULL (whole-table access requested), this doesn't
+ * necessarily guarantee that the user can read all columns of a child
+ * table, but we allow it anyway (see comments in examine_variable()) and
+ * don't bother checking any column privileges.
+ */
+ if (pg_class_aclcheck(rte->relid, userid, ACL_SELECT) == ACLCHECK_OK)
+ return true;
+
+ if (varattnos == NULL)
+ return false; /* whole-table access requested */
+
+ /*
+ * Don't have table-level SELECT privilege, so check per-column
+ * privileges.
+ */
+ varattno = -1;
+ while ((varattno = bms_next_member(varattnos, varattno)) >= 0)
+ {
+ AttrNumber attno = varattno + FirstLowInvalidHeapAttributeNumber;
+
+ if (attno == InvalidAttrNumber)
+ {
+ /* Whole-row reference, so must have access to all columns */
+ if (pg_attribute_aclcheck_all(rte->relid, userid, ACL_SELECT,
+ ACLMASK_ALL) != ACLCHECK_OK)
+ return false;
+ }
+ else
+ {
+ if (pg_attribute_aclcheck(rte->relid, attno, userid,
+ ACL_SELECT) != ACLCHECK_OK)
+ return false;
+ }
+ }
+
+ /* If we reach here, have all required column privileges */
+ return true;
+}
+
/*
* Check whether it is permitted to call func_oid passing some of the
- * pg_statistic data in vardata. We allow this either if the user has SELECT
- * privileges on the table or column underlying the pg_statistic data or if
- * the function is marked leak-proof.
+ * pg_statistic data in vardata. We allow this if either of the following
+ * conditions is met: (1) the user has SELECT privileges on the table or
+ * column underlying the pg_statistic data and there are no securityQuals from
+ * security barrier views or RLS policies, or (2) the function is marked
+ * leakproof.
*/
bool
statistic_proc_security_check(VariableStatData *vardata, Oid func_oid)
{
if (vardata->acl_ok)
- return true;
+ return true; /* have SELECT privs and no securityQuals */
if (!OidIsValid(func_oid))
return false;
extern void standard_ExecutorEnd(QueryDesc *queryDesc);
extern void ExecutorRewind(QueryDesc *queryDesc);
extern bool ExecCheckRTPerms(List *rangeTable, bool ereport_on_violation);
+extern bool ExecCheckRTEPerms(RangeTblEntry *rte);
extern void CheckValidResultRel(ResultRelInfo *resultRelInfo, CmdType operation);
extern void InitResultRelInfo(ResultRelInfo *resultRelInfo,
Relation resultRelationDesc,
Oid atttype; /* actual type (after stripping relabel) */
int32 atttypmod; /* actual typmod (after stripping relabel) */
bool isunique; /* matches unique index or DISTINCT clause */
- bool acl_ok; /* result of ACL check on table or column */
+ bool acl_ok; /* true if user has SELECT privilege on all
+ * rows from the table or column */
} VariableStatData;
#define ReleaseVariableStats(vardata) \
extern void examine_variable(PlannerInfo *root, Node *node, int varRelid,
VariableStatData *vardata);
+extern bool all_rows_selectable(PlannerInfo *root, Index varno, Bitmapset *varattnos);
extern bool statistic_proc_security_check(VariableStatData *vardata, Oid func_oid);
extern bool get_restriction_variable(PlannerInfo *root, List *args,
int varRelid,
SELECT * FROM atest12 WHERE b <<< 5;
CREATE VIEW atest12sbv WITH (security_barrier=true) AS
SELECT * FROM atest12 WHERE b <<< 5;
-GRANT SELECT ON atest12v TO PUBLIC;
-GRANT SELECT ON atest12sbv TO PUBLIC;
-- This plan should use nestloop, knowing that few rows will be selected.
EXPLAIN (COSTS OFF) SELECT * FROM atest12v x, atest12v y WHERE x.a = y.b;
QUERY PLAN
LANGUAGE plpgsql immutable;
CREATE OPERATOR >>> (procedure = leak2, leftarg = integer, rightarg = integer,
restrict = scalargtsel);
--- This should not show any "leak" notices before failing.
+-- These should not show any "leak" notices before failing.
EXPLAIN (COSTS OFF) SELECT * FROM atest12 WHERE a >>> 0;
ERROR: permission denied for table atest12
+EXPLAIN (COSTS OFF) SELECT * FROM atest12v WHERE a >>> 0;
+ERROR: permission denied for view atest12v
+EXPLAIN (COSTS OFF) SELECT * FROM atest12sbv WHERE a >>> 0;
+ERROR: permission denied for view atest12sbv
+-- Now regress_priv_user1 grants access to regress_priv_user2 via the views.
+SET SESSION AUTHORIZATION regress_priv_user1;
+GRANT SELECT ON atest12v TO PUBLIC;
+GRANT SELECT ON atest12sbv TO PUBLIC;
+SET SESSION AUTHORIZATION regress_priv_user2;
-- These plans should continue to use a nestloop, since they execute with the
-- privileges of the view owner.
EXPLAIN (COSTS OFF) SELECT * FROM atest12v x, atest12v y WHERE x.a = y.b;
DROP VIEW rls_view;
DROP TABLE rls_tbl;
DROP TABLE ref_tbl;
--- Leaky operator test
+-- Leaky operator tests
CREATE TABLE rls_tbl (a int);
INSERT INTO rls_tbl SELECT x/10 FROM generate_series(1, 100) x;
ANALYZE rls_tbl;
---
(0 rows)
+RESET SESSION AUTHORIZATION;
+CREATE TABLE rls_child_tbl () INHERITS (rls_tbl);
+INSERT INTO rls_child_tbl SELECT x/10 FROM generate_series(1, 100) x;
+ANALYZE rls_child_tbl;
+CREATE TABLE rls_ptbl (a int) PARTITION BY RANGE (a);
+CREATE TABLE rls_part PARTITION OF rls_ptbl FOR VALUES FROM (-100) TO (100);
+INSERT INTO rls_ptbl SELECT x/10 FROM generate_series(1, 100) x;
+ANALYZE rls_ptbl, rls_part;
+ALTER TABLE rls_ptbl ENABLE ROW LEVEL SECURITY;
+ALTER TABLE rls_part ENABLE ROW LEVEL SECURITY;
+GRANT SELECT ON rls_ptbl TO regress_rls_alice;
+GRANT SELECT ON rls_part TO regress_rls_alice;
+CREATE POLICY p1 ON rls_tbl USING (a < 0);
+CREATE POLICY p2 ON rls_ptbl USING (a < 0);
+CREATE POLICY p3 ON rls_part USING (a < 0);
+SET SESSION AUTHORIZATION regress_rls_alice;
+SELECT * FROM rls_tbl WHERE a <<< 1000;
+ a
+---
+(0 rows)
+
+SELECT * FROM rls_child_tbl WHERE a <<< 1000;
+ERROR: permission denied for table rls_child_tbl
+SELECT * FROM rls_ptbl WHERE a <<< 1000;
+ a
+---
+(0 rows)
+
+SELECT * FROM rls_part WHERE a <<< 1000;
+ a
+---
+(0 rows)
+
+SELECT * FROM (SELECT * FROM rls_tbl UNION ALL
+ SELECT * FROM rls_tbl) t WHERE a <<< 1000;
+ a
+---
+(0 rows)
+
+SELECT * FROM (SELECT * FROM rls_child_tbl UNION ALL
+ SELECT * FROM rls_child_tbl) t WHERE a <<< 1000;
+ERROR: permission denied for table rls_child_tbl
+RESET SESSION AUTHORIZATION;
+REVOKE SELECT ON rls_tbl FROM regress_rls_alice;
+CREATE VIEW rls_tbl_view AS SELECT * FROM rls_tbl;
+ALTER TABLE rls_child_tbl ENABLE ROW LEVEL SECURITY;
+GRANT SELECT ON rls_child_tbl TO regress_rls_alice;
+CREATE POLICY p4 ON rls_child_tbl USING (a < 0);
+SET SESSION AUTHORIZATION regress_rls_alice;
+SELECT * FROM rls_tbl WHERE a <<< 1000;
+ERROR: permission denied for table rls_tbl
+SELECT * FROM rls_tbl_view WHERE a <<< 1000;
+ERROR: permission denied for view rls_tbl_view
+SELECT * FROM rls_child_tbl WHERE a <<< 1000;
+ a
+---
+(0 rows)
+
+SELECT * FROM (SELECT * FROM rls_tbl UNION ALL
+ SELECT * FROM rls_tbl) t WHERE a <<< 1000;
+ERROR: permission denied for table rls_tbl
+SELECT * FROM (SELECT * FROM rls_child_tbl UNION ALL
+ SELECT * FROM rls_child_tbl) t WHERE a <<< 1000;
+ a
+---
+(0 rows)
+
DROP OPERATOR <<< (int, int);
DROP FUNCTION op_leak(int, int);
RESET SESSION AUTHORIZATION;
+DROP TABLE rls_part;
+DROP TABLE rls_ptbl;
+DROP TABLE rls_child_tbl;
+DROP VIEW rls_tbl_view;
DROP TABLE rls_tbl;
-- Bug #16006: whole-row Vars in a policy don't play nice with sub-selects
SET SESSION AUTHORIZATION regress_rls_alice;
LANGUAGE plpgsql;
CREATE OPERATOR <<< (procedure = op_leak, leftarg = int, rightarg = int,
restrict = scalarltsel);
+CREATE FUNCTION op_leak(record, record) RETURNS bool
+ AS 'BEGIN RAISE NOTICE ''op_leak => %, %'', $1, $2; RETURN $1 < $2; END'
+ LANGUAGE plpgsql;
+CREATE OPERATOR <<< (procedure = op_leak, leftarg = record, rightarg = record,
+ restrict = scalarltsel);
SELECT * FROM tststats.priv_test_tbl WHERE a <<< 0 AND b <<< 0; -- Permission denied
ERROR: permission denied for table priv_test_tbl
+SELECT * FROM tststats.priv_test_tbl t
+ WHERE a <<< 0 AND (b <<< 0 OR t.* <<< (1, 1) IS NOT NULL); -- Permission denied
+ERROR: permission denied for table priv_test_tbl
DELETE FROM tststats.priv_test_tbl WHERE a <<< 0 AND b <<< 0; -- Permission denied
ERROR: permission denied for table priv_test_tbl
-- Grant access via a security barrier view, but hide all data
---+---
(0 rows)
+SELECT * FROM tststats.priv_test_view t
+ WHERE a <<< 0 AND (b <<< 0 OR t.* <<< (1, 1) IS NOT NULL); -- Should not leak
+ a | b
+---+---
+(0 rows)
+
DELETE FROM tststats.priv_test_view WHERE a <<< 0 AND b <<< 0; -- Should not leak
-- Grant table access, but hide all data with RLS
RESET SESSION AUTHORIZATION;
ALTER TABLE tststats.priv_test_tbl ENABLE ROW LEVEL SECURITY;
+CREATE POLICY priv_test_tbl_pol ON tststats.priv_test_tbl USING (2 * a < 0);
GRANT SELECT, DELETE ON tststats.priv_test_tbl TO regress_stats_user1;
-- Should now have direct table access, but see nothing and leak nothing
SET SESSION AUTHORIZATION regress_stats_user1;
---+---
(0 rows)
+SELECT * FROM tststats.priv_test_tbl t
+ WHERE a <<< 0 AND (b <<< 0 OR t.* <<< (1, 1) IS NOT NULL); -- Should not leak
+ a | b
+---+---
+(0 rows)
+
DELETE FROM tststats.priv_test_tbl WHERE a <<< 0 AND b <<< 0; -- Should not leak
+-- Create plain inheritance parent table with no access permissions
+RESET SESSION AUTHORIZATION;
+CREATE TABLE tststats.priv_test_parent_tbl (a int, b int);
+ALTER TABLE tststats.priv_test_tbl INHERIT tststats.priv_test_parent_tbl;
+-- Should not have access to parent, and should leak nothing
+SET SESSION AUTHORIZATION regress_stats_user1;
+SELECT * FROM tststats.priv_test_parent_tbl WHERE a <<< 0 AND b <<< 0; -- Permission denied
+ERROR: permission denied for table priv_test_parent_tbl
+SELECT * FROM tststats.priv_test_parent_tbl t
+ WHERE a <<< 0 AND (b <<< 0 OR t.* <<< (1, 1) IS NOT NULL); -- Permission denied
+ERROR: permission denied for table priv_test_parent_tbl
+DELETE FROM tststats.priv_test_parent_tbl WHERE a <<< 0 AND b <<< 0; -- Permission denied
+ERROR: permission denied for table priv_test_parent_tbl
+-- Grant table access to parent, but hide all data with RLS
+RESET SESSION AUTHORIZATION;
+ALTER TABLE tststats.priv_test_parent_tbl ENABLE ROW LEVEL SECURITY;
+CREATE POLICY priv_test_parent_tbl_pol ON tststats.priv_test_parent_tbl USING (2 * a < 0);
+GRANT SELECT, DELETE ON tststats.priv_test_parent_tbl TO regress_stats_user1;
+-- Should now have direct table access to parent, but see nothing and leak nothing
+SET SESSION AUTHORIZATION regress_stats_user1;
+SELECT * FROM tststats.priv_test_parent_tbl WHERE a <<< 0 AND b <<< 0; -- Should not leak
+ a | b
+---+---
+(0 rows)
+
+SELECT * FROM tststats.priv_test_parent_tbl t
+ WHERE a <<< 0 AND (b <<< 0 OR t.* <<< (1, 1) IS NOT NULL); -- Should not leak
+ a | b
+---+---
+(0 rows)
+
+DELETE FROM tststats.priv_test_parent_tbl WHERE a <<< 0 AND b <<< 0; -- Should not leak
-- privilege checks for pg_stats_ext and pg_stats_ext_exprs
RESET SESSION AUTHORIZATION;
CREATE TABLE stats_ext_tbl (id INT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, col TEXT);
-- Tidy up
DROP OPERATOR <<< (int, int);
DROP FUNCTION op_leak(int, int);
+DROP OPERATOR <<< (record, record);
+DROP FUNCTION op_leak(record, record);
RESET SESSION AUTHORIZATION;
DROP TABLE stats_ext_tbl;
DROP SCHEMA tststats CASCADE;
-NOTICE: drop cascades to 2 other objects
-DETAIL: drop cascades to table tststats.priv_test_tbl
+NOTICE: drop cascades to 3 other objects
+DETAIL: drop cascades to table tststats.priv_test_parent_tbl
+drop cascades to table tststats.priv_test_tbl
drop cascades to view tststats.priv_test_view
DROP USER regress_stats_user1;
SELECT * FROM atest12 WHERE b <<< 5;
CREATE VIEW atest12sbv WITH (security_barrier=true) AS
SELECT * FROM atest12 WHERE b <<< 5;
-GRANT SELECT ON atest12v TO PUBLIC;
-GRANT SELECT ON atest12sbv TO PUBLIC;
-- This plan should use nestloop, knowing that few rows will be selected.
EXPLAIN (COSTS OFF) SELECT * FROM atest12v x, atest12v y WHERE x.a = y.b;
CREATE OPERATOR >>> (procedure = leak2, leftarg = integer, rightarg = integer,
restrict = scalargtsel);
--- This should not show any "leak" notices before failing.
+-- These should not show any "leak" notices before failing.
EXPLAIN (COSTS OFF) SELECT * FROM atest12 WHERE a >>> 0;
+EXPLAIN (COSTS OFF) SELECT * FROM atest12v WHERE a >>> 0;
+EXPLAIN (COSTS OFF) SELECT * FROM atest12sbv WHERE a >>> 0;
+
+-- Now regress_priv_user1 grants access to regress_priv_user2 via the views.
+SET SESSION AUTHORIZATION regress_priv_user1;
+GRANT SELECT ON atest12v TO PUBLIC;
+GRANT SELECT ON atest12sbv TO PUBLIC;
+SET SESSION AUTHORIZATION regress_priv_user2;
-- These plans should continue to use a nestloop, since they execute with the
-- privileges of the view owner.
DROP TABLE rls_tbl;
DROP TABLE ref_tbl;
--- Leaky operator test
+-- Leaky operator tests
CREATE TABLE rls_tbl (a int);
INSERT INTO rls_tbl SELECT x/10 FROM generate_series(1, 100) x;
ANALYZE rls_tbl;
CREATE OPERATOR <<< (procedure = op_leak, leftarg = int, rightarg = int,
restrict = scalarltsel);
SELECT * FROM rls_tbl WHERE a <<< 1000;
+RESET SESSION AUTHORIZATION;
+
+CREATE TABLE rls_child_tbl () INHERITS (rls_tbl);
+INSERT INTO rls_child_tbl SELECT x/10 FROM generate_series(1, 100) x;
+ANALYZE rls_child_tbl;
+
+CREATE TABLE rls_ptbl (a int) PARTITION BY RANGE (a);
+CREATE TABLE rls_part PARTITION OF rls_ptbl FOR VALUES FROM (-100) TO (100);
+INSERT INTO rls_ptbl SELECT x/10 FROM generate_series(1, 100) x;
+ANALYZE rls_ptbl, rls_part;
+
+ALTER TABLE rls_ptbl ENABLE ROW LEVEL SECURITY;
+ALTER TABLE rls_part ENABLE ROW LEVEL SECURITY;
+GRANT SELECT ON rls_ptbl TO regress_rls_alice;
+GRANT SELECT ON rls_part TO regress_rls_alice;
+CREATE POLICY p1 ON rls_tbl USING (a < 0);
+CREATE POLICY p2 ON rls_ptbl USING (a < 0);
+CREATE POLICY p3 ON rls_part USING (a < 0);
+
+SET SESSION AUTHORIZATION regress_rls_alice;
+SELECT * FROM rls_tbl WHERE a <<< 1000;
+SELECT * FROM rls_child_tbl WHERE a <<< 1000;
+SELECT * FROM rls_ptbl WHERE a <<< 1000;
+SELECT * FROM rls_part WHERE a <<< 1000;
+SELECT * FROM (SELECT * FROM rls_tbl UNION ALL
+ SELECT * FROM rls_tbl) t WHERE a <<< 1000;
+SELECT * FROM (SELECT * FROM rls_child_tbl UNION ALL
+ SELECT * FROM rls_child_tbl) t WHERE a <<< 1000;
+RESET SESSION AUTHORIZATION;
+
+REVOKE SELECT ON rls_tbl FROM regress_rls_alice;
+CREATE VIEW rls_tbl_view AS SELECT * FROM rls_tbl;
+
+ALTER TABLE rls_child_tbl ENABLE ROW LEVEL SECURITY;
+GRANT SELECT ON rls_child_tbl TO regress_rls_alice;
+CREATE POLICY p4 ON rls_child_tbl USING (a < 0);
+
+SET SESSION AUTHORIZATION regress_rls_alice;
+SELECT * FROM rls_tbl WHERE a <<< 1000;
+SELECT * FROM rls_tbl_view WHERE a <<< 1000;
+SELECT * FROM rls_child_tbl WHERE a <<< 1000;
+SELECT * FROM (SELECT * FROM rls_tbl UNION ALL
+ SELECT * FROM rls_tbl) t WHERE a <<< 1000;
+SELECT * FROM (SELECT * FROM rls_child_tbl UNION ALL
+ SELECT * FROM rls_child_tbl) t WHERE a <<< 1000;
DROP OPERATOR <<< (int, int);
DROP FUNCTION op_leak(int, int);
RESET SESSION AUTHORIZATION;
+DROP TABLE rls_part;
+DROP TABLE rls_ptbl;
+DROP TABLE rls_child_tbl;
+DROP VIEW rls_tbl_view;
DROP TABLE rls_tbl;
-- Bug #16006: whole-row Vars in a policy don't play nice with sub-selects
LANGUAGE plpgsql;
CREATE OPERATOR <<< (procedure = op_leak, leftarg = int, rightarg = int,
restrict = scalarltsel);
+CREATE FUNCTION op_leak(record, record) RETURNS bool
+ AS 'BEGIN RAISE NOTICE ''op_leak => %, %'', $1, $2; RETURN $1 < $2; END'
+ LANGUAGE plpgsql;
+CREATE OPERATOR <<< (procedure = op_leak, leftarg = record, rightarg = record,
+ restrict = scalarltsel);
SELECT * FROM tststats.priv_test_tbl WHERE a <<< 0 AND b <<< 0; -- Permission denied
+SELECT * FROM tststats.priv_test_tbl t
+ WHERE a <<< 0 AND (b <<< 0 OR t.* <<< (1, 1) IS NOT NULL); -- Permission denied
DELETE FROM tststats.priv_test_tbl WHERE a <<< 0 AND b <<< 0; -- Permission denied
-- Grant access via a security barrier view, but hide all data
-- Should now have access via the view, but see nothing and leak nothing
SET SESSION AUTHORIZATION regress_stats_user1;
SELECT * FROM tststats.priv_test_view WHERE a <<< 0 AND b <<< 0; -- Should not leak
+SELECT * FROM tststats.priv_test_view t
+ WHERE a <<< 0 AND (b <<< 0 OR t.* <<< (1, 1) IS NOT NULL); -- Should not leak
DELETE FROM tststats.priv_test_view WHERE a <<< 0 AND b <<< 0; -- Should not leak
-- Grant table access, but hide all data with RLS
RESET SESSION AUTHORIZATION;
ALTER TABLE tststats.priv_test_tbl ENABLE ROW LEVEL SECURITY;
+CREATE POLICY priv_test_tbl_pol ON tststats.priv_test_tbl USING (2 * a < 0);
GRANT SELECT, DELETE ON tststats.priv_test_tbl TO regress_stats_user1;
-- Should now have direct table access, but see nothing and leak nothing
SET SESSION AUTHORIZATION regress_stats_user1;
SELECT * FROM tststats.priv_test_tbl WHERE a <<< 0 AND b <<< 0; -- Should not leak
+SELECT * FROM tststats.priv_test_tbl t
+ WHERE a <<< 0 AND (b <<< 0 OR t.* <<< (1, 1) IS NOT NULL); -- Should not leak
DELETE FROM tststats.priv_test_tbl WHERE a <<< 0 AND b <<< 0; -- Should not leak
+-- Create plain inheritance parent table with no access permissions
+RESET SESSION AUTHORIZATION;
+CREATE TABLE tststats.priv_test_parent_tbl (a int, b int);
+ALTER TABLE tststats.priv_test_tbl INHERIT tststats.priv_test_parent_tbl;
+
+-- Should not have access to parent, and should leak nothing
+SET SESSION AUTHORIZATION regress_stats_user1;
+SELECT * FROM tststats.priv_test_parent_tbl WHERE a <<< 0 AND b <<< 0; -- Permission denied
+SELECT * FROM tststats.priv_test_parent_tbl t
+ WHERE a <<< 0 AND (b <<< 0 OR t.* <<< (1, 1) IS NOT NULL); -- Permission denied
+DELETE FROM tststats.priv_test_parent_tbl WHERE a <<< 0 AND b <<< 0; -- Permission denied
+
+-- Grant table access to parent, but hide all data with RLS
+RESET SESSION AUTHORIZATION;
+ALTER TABLE tststats.priv_test_parent_tbl ENABLE ROW LEVEL SECURITY;
+CREATE POLICY priv_test_parent_tbl_pol ON tststats.priv_test_parent_tbl USING (2 * a < 0);
+GRANT SELECT, DELETE ON tststats.priv_test_parent_tbl TO regress_stats_user1;
+
+-- Should now have direct table access to parent, but see nothing and leak nothing
+SET SESSION AUTHORIZATION regress_stats_user1;
+SELECT * FROM tststats.priv_test_parent_tbl WHERE a <<< 0 AND b <<< 0; -- Should not leak
+SELECT * FROM tststats.priv_test_parent_tbl t
+ WHERE a <<< 0 AND (b <<< 0 OR t.* <<< (1, 1) IS NOT NULL); -- Should not leak
+DELETE FROM tststats.priv_test_parent_tbl WHERE a <<< 0 AND b <<< 0; -- Should not leak
+
-- privilege checks for pg_stats_ext and pg_stats_ext_exprs
RESET SESSION AUTHORIZATION;
CREATE TABLE stats_ext_tbl (id INT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, col TEXT);
-- Tidy up
DROP OPERATOR <<< (int, int);
DROP FUNCTION op_leak(int, int);
+DROP OPERATOR <<< (record, record);
+DROP FUNCTION op_leak(record, record);
RESET SESSION AUTHORIZATION;
DROP TABLE stats_ext_tbl;
DROP SCHEMA tststats CASCADE;