]> git.ipfire.org Git - thirdparty/postgresql.git/commitdiff
Fix security checks in selectivity estimation functions.
authorDean Rasheed <dean.a.rasheed@gmail.com>
Mon, 11 Aug 2025 08:12:09 +0000 (09:12 +0100)
committerDean Rasheed <dean.a.rasheed@gmail.com>
Mon, 11 Aug 2025 08:12:09 +0000 (09:12 +0100)
Commit e2d4ef8de86 (the fix for CVE-2017-7484) added security checks
to the selectivity estimation functions to prevent them from running
user-supplied operators on data obtained from pg_statistic if the user
lacks privileges to select from the underlying table. In cases
involving inheritance/partitioning, those checks were originally
performed against the child RTE (which for plain inheritance might
actually refer to the parent table). Commit 553d2ec2710 then extended
that to also check the parent RTE, allowing access if the user had
permissions on either the parent or the child. It turns out, however,
that doing any checks using the child RTE is incorrect, since
securityQuals is set to NULL when creating an RTE for an inheritance
child (whether it refers to the parent table or the child table), and
therefore such checks do not correctly account for any RLS policies or
security barrier views. Therefore, do the security checks using only
the parent RTE. This is consistent with how RLS policies are applied,
and the executor's ACL checks, both of which use only the parent
table's permissions/policies. Similar checks are performed in the
extended stats code, so update that in the same way, centralizing all
the checks in a new function.

In addition, note that these checks by themselves are insufficient to
ensure that the user has access to the table's data because, in a
query that goes via a view, they only check that the view owner has
permissions on the underlying table, not that the current user has
permissions on the view itself. In the selectivity estimation
functions, there is no easy way to navigate from underlying tables to
views, so add permissions checks for all views mentioned in the query
to the planner startup code. If the user lacks permissions on a view,
a permissions error will now be reported at planner-startup, and the
selectivity estimation functions will not be run.

Checking view permissions at planner-startup in this way is a little
ugly, since the same checks will be repeated at executor-startup.
Longer-term, it might be better to move all the permissions checks
from the executor to the planner so that permissions errors can be
reported sooner, instead of creating a plan that won't ever be run.
However, such a change seems too far-reaching to be back-patched.

Back-patch to all supported versions. In v13, there is the added
complication that UPDATEs and DELETEs on inherited target tables are
planned using inheritance_planner(), which plans each inheritance
child table separately, so that the selectivity estimation functions
do not know that they are dealing with a child table accessed via its
parent. Handle that by checking access permissions on the top parent
table at planner-startup, in the same way as we do for views. Any
securityQuals on the top parent table are moved down to the child
tables by inheritance_planner(), so they continue to be checked by the
selectivity estimation functions.

Author: Dean Rasheed <dean.a.rasheed@gmail.com>
Reviewed-by: Tom Lane <tgl@sss.pgh.pa.us>
Reviewed-by: Noah Misch <noah@leadboat.com>
Backpatch-through: 13
Security: CVE-2025-8713

12 files changed:
src/backend/executor/execMain.c
src/backend/optimizer/plan/planner.c
src/backend/statistics/extended_stats.c
src/backend/utils/adt/selfuncs.c
src/include/executor/executor.h
src/include/utils/selfuncs.h
src/test/regress/expected/privileges.out
src/test/regress/expected/rowsecurity.out
src/test/regress/expected/stats_ext.out
src/test/regress/sql/privileges.sql
src/test/regress/sql/rowsecurity.sql
src/test/regress/sql/stats_ext.sql

index 1ae17a515ebc9015a23e224abd4f1f551c0f9977..3143fda447740ccb363e7ad0513dad18c984ad21 100644 (file)
@@ -88,7 +88,6 @@ static void ExecutePlan(QueryDesc *queryDesc,
                                                uint64 numberTuples,
                                                ScanDirection direction,
                                                DestReceiver *dest);
-static bool ExecCheckRTEPerms(RangeTblEntry *rte);
 static bool ExecCheckRTEPermsModified(Oid relOid, Oid userid,
                                                                          Bitmapset *modifiedCols,
                                                                          AclMode requiredPerms);
@@ -590,7 +589,7 @@ ExecCheckRTPerms(List *rangeTable, bool ereport_on_violation)
  * ExecCheckRTEPerms
  *             Check access permissions for a single RTE.
  */
-static bool
+bool
 ExecCheckRTEPerms(RangeTblEntry *rte)
 {
        AclMode         requiredPerms;
index 4979f17a9195c8eeeb99fd30754d80d776d3803b..1aa43b9da2897de4e212c3795ca157926331c8f6 100644 (file)
@@ -60,6 +60,7 @@
 #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"
@@ -758,6 +759,35 @@ subquery_planner(PlannerGlobal *glob, Query *parse,
                                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.
index 4212e0cd24915e0096cd153e4e9237bd36fe6dd0..483b266f2827e175ca64ac4ae2b4385e1a9d5040 100644 (file)
@@ -1345,6 +1345,9 @@ choose_best_statistics(List *stats, char requiredkind,
  *             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.
@@ -1352,7 +1355,7 @@ choose_best_statistics(List *stats, char requiredkind,
 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))
@@ -1387,7 +1390,6 @@ statext_is_compatible_clause_internal(PlannerInfo *root, Node *clause,
        /* (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;
 
@@ -1422,24 +1424,15 @@ statext_is_compatible_clause_internal(PlannerInfo *root, Node *clause,
                                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);
@@ -1449,7 +1442,6 @@ statext_is_compatible_clause_internal(PlannerInfo *root, Node *clause,
        /* Var/Expr IN Array */
        if (IsA(clause, ScalarArrayOpExpr))
        {
-               RangeTblEntry *rte = root->simple_rte_array[relid];
                ScalarArrayOpExpr *expr = (ScalarArrayOpExpr *) clause;
                Node       *clause_expr;
                bool            expronleft;
@@ -1489,24 +1481,15 @@ statext_is_compatible_clause_internal(PlannerInfo *root, Node *clause,
                                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);
@@ -1543,7 +1526,8 @@ statext_is_compatible_clause_internal(PlannerInfo *root, Node *clause,
                         */
                        if (!statext_is_compatible_clause_internal(root,
                                                                                                           (Node *) lfirst(lc),
-                                                                                                          relid, attnums, exprs))
+                                                                                                          relid, attnums, exprs,
+                                                                                                          leakproof))
                                return false;
                }
 
@@ -1557,8 +1541,10 @@ statext_is_compatible_clause_internal(PlannerInfo *root, Node *clause,
 
                /* 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);
@@ -1597,10 +1583,9 @@ static bool
 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
@@ -1640,19 +1625,31 @@ statext_is_compatible_clause(PlannerInfo *root, Node *clause, Index relid,
                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;
@@ -1677,26 +1674,9 @@ statext_is_compatible_clause(PlannerInfo *root, Node *clause, Index relid,
                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 */
index 87879c9ddc8fa197a90066888f52b129bc179a60..6a8d2fd7a8c736324effafb227536f4afdc51691 100644 (file)
@@ -4958,8 +4958,8 @@ ReleaseDummy(HeapTuple tuple)
  *             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.
@@ -5138,78 +5138,32 @@ examine_variable(PlannerInfo *root, Node *node, int varRelid,
 
                                                        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
                                                        {
@@ -5278,10 +5232,6 @@ examine_variable(PlannerInfo *root, Node *node, int varRelid,
                                {
                                        HeapTuple       t = statext_expressions_load(info->statOid, pos);
 
-                                       /* Get statistics object's table for permission check */
-                                       RangeTblEntry *rte;
-                                       Oid                     userid;
-
                                        vardata->statsTuple = t;
 
                                        /*
@@ -5290,70 +5240,27 @@ examine_variable(PlannerInfo *root, Node *node, int varRelid,
                                         */
                                        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;
                                }
@@ -5406,92 +5313,20 @@ examine_simple_variable(PlannerInfo *root, Var *var,
 
                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
                {
@@ -5615,17 +5450,208 @@ examine_simple_variable(PlannerInfo *root, Var *var,
        }
 }
 
+/*
+ * 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;
index cc97cf8cc7f37ca839a02e4ae556a53f17d7c69f..9de6b9c6c4933cc573825924d2f72b781741215c 100644 (file)
@@ -197,6 +197,7 @@ extern void ExecutorEnd(QueryDesc *queryDesc);
 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,
index 9dd444e1ff5dada05bafb0e2d56ac5e161544d5e..7950b3b68c507285c2819fdc3d189faba7ef854c 100644 (file)
@@ -95,7 +95,8 @@ typedef struct VariableStatData
        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)  \
@@ -149,6 +150,7 @@ extern PGDLLIMPORT get_index_stats_hook_type get_index_stats_hook;
 
 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,
index 7c9aa84a58536f508b58c4c8a80e97fdecb63e32..08c6c3bc59ab53f067e63819705901620c9cce0b 100644 (file)
@@ -325,8 +325,6 @@ CREATE VIEW atest12v AS
   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                    
@@ -372,9 +370,18 @@ CREATE FUNCTION leak2(integer,integer) RETURNS boolean
   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;
index bcb591929f46082e321368656b341571eb5efdbd..75b93a10f56328d4b008312092cc7686c8d19520 100644 (file)
@@ -3964,7 +3964,7 @@ RESET SESSION AUTHORIZATION;
 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;
@@ -3981,9 +3981,80 @@ SELECT * FROM rls_tbl WHERE a <<< 1000;
 ---
 (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;
index dae49dbc246a1006106f4629017000e8f749a84c..2471c14385f861a59429538f1c1ad9475192b6b5 100644 (file)
@@ -3216,8 +3216,16 @@ CREATE FUNCTION op_leak(int, int) RETURNS bool
     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
@@ -3232,10 +3240,17 @@ SELECT * FROM tststats.priv_test_view WHERE a <<< 0 AND b <<< 0; -- Should not l
 ---+---
 (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;
@@ -3244,7 +3259,45 @@ SELECT * FROM tststats.priv_test_tbl WHERE a <<< 0 AND b <<< 0; -- Should not le
 ---+---
 (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);
@@ -3290,10 +3343,13 @@ SELECT statistics_name, most_common_vals FROM pg_stats_ext_exprs x
 -- 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;
index ab42f511d74179b5ccb837861c8671288566310c..f351583a11eaa78d1738fff38624f47404a58f59 100644 (file)
@@ -227,8 +227,6 @@ CREATE VIEW atest12v AS
   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;
@@ -250,8 +248,16 @@ CREATE FUNCTION leak2(integer,integer) RETURNS boolean
 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.
index 48ed6ef703f7d3da1cc827f18d49dc81d4ba2dd4..fdcf78ef2bf982e36ca962ae22f40f3151e354e6 100644 (file)
@@ -1834,7 +1834,7 @@ 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;
@@ -1849,9 +1849,58 @@ CREATE FUNCTION op_leak(int, int) RETURNS bool
 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
index 53907f75e748b97d52e69366458886d728d3a0d4..9b2b54813d9b67f42d9589c9574af536a58ddf15 100644 (file)
@@ -1623,7 +1623,14 @@ CREATE FUNCTION op_leak(int, int) RETURNS bool
     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
@@ -1635,18 +1642,48 @@ GRANT SELECT, DELETE ON tststats.priv_test_view TO regress_stats_user1;
 -- 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);
@@ -1676,6 +1713,8 @@ SELECT statistics_name, most_common_vals FROM pg_stats_ext_exprs x
 -- 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;