diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index 876a87ff52..ea4d4c55cb 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -790,6 +790,7 @@ inheritance_planner(PlannerInfo *root) { Query *parse = root->parse; int parentRTindex = parse->resultRelation; + Bitmapset *resultRTindexes = NULL; int nominalRelation = -1; List *final_rtable = NIL; int save_rel_array_size = 0; @@ -815,7 +816,21 @@ inheritance_planner(PlannerInfo *root) * (1) would result in a rangetable of length O(N^2) for N targets, with * at least O(N^3) work expended here; and (2) would greatly complicate * management of the rowMarks list. + * + * Note that any RTEs with security barrier quals will be turned into + * subqueries during planning, and so we must create copies of them too, + * except where they are target relations, which will each only be used + * in a single plan. */ + resultRTindexes = bms_add_member(resultRTindexes, parentRTindex); + foreach(lc, root->append_rel_list) + { + AppendRelInfo *appinfo = (AppendRelInfo *) lfirst(lc); + if (appinfo->parent_relid == parentRTindex) + resultRTindexes = bms_add_member(resultRTindexes, + appinfo->child_relid); + } + foreach(lc, root->append_rel_list) { AppendRelInfo *appinfo = (AppendRelInfo *) lfirst(lc); @@ -886,21 +901,29 @@ inheritance_planner(PlannerInfo *root) { RangeTblEntry *rte = (RangeTblEntry *) lfirst(lr); - if (rte->rtekind == RTE_SUBQUERY) + /* + * Copy subquery RTEs and RTEs with security barrier quals + * that will be turned into subqueries, except those that are + * target relations. + */ + if (rte->rtekind == RTE_SUBQUERY || + (rte->securityQuals != NIL && + !bms_is_member(rti, resultRTindexes))) { Index newrti; /* * The RTE can't contain any references to its own RT - * index, so we can save a few cycles by applying - * ChangeVarNodes before we append the RTE to the - * rangetable. + * index, except in the security barrier quals, so we can + * save a few cycles by applying ChangeVarNodes before we + * append the RTE to the rangetable. */ newrti = list_length(subroot.parse->rtable) + 1; ChangeVarNodes((Node *) subroot.parse, rti, newrti, 0); ChangeVarNodes((Node *) subroot.rowMarks, rti, newrti, 0); ChangeVarNodes((Node *) subroot.append_rel_list, rti, newrti, 0); rte = copyObject(rte); + ChangeVarNodes((Node *) rte->securityQuals, rti, newrti, 0); subroot.parse->rtable = lappend(subroot.parse->rtable, rte); } @@ -2283,7 +2306,19 @@ select_rowmark_type(RangeTblEntry *rte, LockClauseStrength strength) switch (strength) { case LCS_NONE: - /* don't need tuple lock, only ability to re-fetch the row */ + /* + * We don't need a tuple lock, only the ability to re-fetch + * the row. Regular tables support ROW_MARK_REFERENCE, but if + * this RTE has security barrier quals, it will be turned into + * a subquery during planning, so use ROW_MARK_COPY. + * + * This is only necessary for LCS_NONE, since real tuple locks + * on an RTE with security barrier quals are supported by + * pushing the lock down into the subquery --- see + * expand_security_qual. + */ + if (rte->securityQuals != NIL) + return ROW_MARK_COPY; return ROW_MARK_REFERENCE; break; case LCS_FORKEYSHARE: diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c index 9d2c280b32..08ec13ccdd 100644 --- a/src/backend/rewrite/rewriteHandler.c +++ b/src/backend/rewrite/rewriteHandler.c @@ -1714,51 +1714,6 @@ fireRIRrules(Query *parsetree, List *activeRIRs, bool forUpdatePushedDown) activeRIRs = list_delete_first(activeRIRs); } } - /* - * If the RTE has row security quals, apply them and recurse into the - * securityQuals. - */ - if (prepend_row_security_policies(parsetree, rte, rt_index)) - { - /* - * We applied security quals, check for infinite recursion and - * then expand any nested queries. - */ - if (list_member_oid(activeRIRs, RelationGetRelid(rel))) - ereport(ERROR, - (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), - errmsg("infinite recursion detected in policy for relation \"%s\"", - RelationGetRelationName(rel)))); - - /* - * Make sure we check for recursion in either securityQuals or - * WITH CHECK quals. - */ - if (rte->securityQuals != NIL) - { - activeRIRs = lcons_oid(RelationGetRelid(rel), activeRIRs); - - expression_tree_walker( (Node*) rte->securityQuals, - fireRIRonSubLink, (void*)activeRIRs ); - - activeRIRs = list_delete_first(activeRIRs); - } - - if (parsetree->withCheckOptions != NIL) - { - WithCheckOption *wco; - List *quals = NIL; - - wco = (WithCheckOption *) makeNode(WithCheckOption); - quals = lcons(wco->qual, quals); - - activeRIRs = lcons_oid(RelationGetRelid(rel), activeRIRs); - - expression_tree_walker( (Node*) quals, fireRIRonSubLink, - (void*)activeRIRs); - } - - } heap_close(rel, NoLock); } @@ -1780,6 +1735,88 @@ fireRIRrules(Query *parsetree, List *activeRIRs, bool forUpdatePushedDown) query_tree_walker(parsetree, fireRIRonSubLink, (void *) activeRIRs, QTW_IGNORE_RC_SUBQUERIES); + /* + * Apply any row level security policies. We do this last because it + * requires special recursion detection if the new quals have sublink + * subqueries, and if we did it in the loop above query_tree_walker + * would then recurse into those quals a second time. + */ + rt_index = 0; + foreach(lc, parsetree->rtable) + { + RangeTblEntry *rte = (RangeTblEntry *) lfirst(lc); + Relation rel; + List *securityQuals; + List *withCheckOptions; + bool hasRowSecurity; + bool hasSubLinks; + + ++rt_index; + + /* Only normal relations can have RLS policies */ + if (rte->rtekind != RTE_RELATION || + rte->relkind != RELKIND_RELATION) + continue; + + rel = heap_open(rte->relid, NoLock); + + /* + * Fetch any new security quals that must be applied to this RTE. + */ + get_row_security_policies(parsetree, rte, rt_index, + &securityQuals, &withCheckOptions, + &hasRowSecurity, &hasSubLinks); + + if (securityQuals != NIL || withCheckOptions != NIL) + { + if (hasSubLinks) + { + /* + * Recursively process the new quals, checking for infinite + * recursion. + */ + if (list_member_oid(activeRIRs, RelationGetRelid(rel))) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("infinite recursion detected in policy for relation \"%s\"", + RelationGetRelationName(rel)))); + + activeRIRs = lcons_oid(RelationGetRelid(rel), activeRIRs); + + expression_tree_walker( (Node*) securityQuals, + fireRIRonSubLink, (void*)activeRIRs ); + + expression_tree_walker( (Node*) withCheckOptions, + fireRIRonSubLink, (void*)activeRIRs ); + + activeRIRs = list_delete_first(activeRIRs); + } + + /* + * Add the new security quals to the start of the RTE's list so + * that they get applied before any existing security quals (which + * might have come from a user-written security barrier view, and + * might contain malicious code). + */ + rte->securityQuals = list_concat(securityQuals, + rte->securityQuals); + + parsetree->withCheckOptions = list_concat(withCheckOptions, + parsetree->withCheckOptions); + } + + /* + * Make sure the query is marked correctly if row level security + * applies, or if the new quals had sublinks. + */ + if (hasRowSecurity) + parsetree->hasRowSecurity = true; + if (hasSubLinks) + parsetree->hasSubLinks = true; + + heap_close(rel, NoLock); + } + return parsetree; } diff --git a/src/backend/rewrite/rowsecurity.c b/src/backend/rewrite/rowsecurity.c index 7669130e2b..bad166ac3a 100644 --- a/src/backend/rewrite/rowsecurity.c +++ b/src/backend/rewrite/rowsecurity.c @@ -58,46 +58,63 @@ static List *pull_row_security_policies(CmdType cmd, Relation relation, Oid user_id); -static void process_policies(List *policies, int rt_index, +static void process_policies(Query* root, List *policies, int rt_index, Expr **final_qual, Expr **final_with_check_qual, - bool *hassublinks); + bool *hassublinks, + BoolExprType boolop); static bool check_role_for_policy(ArrayType *policy_roles, Oid user_id); /* - * hook to allow extensions to apply their own security policy + * hooks to allow extensions to add their own security policies + * + * row_security_policy_hook_permissive can be used to add policies which + * are included in the "OR"d set of policies. + * + * row_security_policy_hook_restrictive can be used to add policies which + * are enforced, regardless of other policies (they are "AND"d). * * See below where the hook is called in prepend_row_security_policies for * insight into how to use this hook. */ -row_security_policy_hook_type row_security_policy_hook = NULL; +row_security_policy_hook_type row_security_policy_hook_permissive = NULL; +row_security_policy_hook_type row_security_policy_hook_restrictive = NULL; /* - * Check the given RTE to see whether it's already had row security quals - * expanded and, if not, prepend any row security rules from built-in or - * plug-in sources to the securityQuals. The security quals are rewritten (for - * view expansion, etc) before being added to the RTE. + * Get any row security quals and check quals that should be applied to the + * specified RTE. * - * Returns true if any quals were added. Note that quals may have been found - * but not added if user rights make the user exempt from row security. + * In addition, hasRowSecurity is set to true if row level security is enabled + * (even if this RTE doesn't have any row security quals), and hasSubLinks is + * set to true if any of the quals returned contain sublinks. */ -bool -prepend_row_security_policies(Query* root, RangeTblEntry* rte, int rt_index) +void +get_row_security_policies(Query* root, RangeTblEntry* rte, int rt_index, + List **securityQuals, List **withCheckOptions, + bool *hasRowSecurity, bool *hasSubLinks) { Expr *rowsec_expr = NULL; Expr *rowsec_with_check_expr = NULL; - Expr *hook_expr = NULL; - Expr *hook_with_check_expr = NULL; + Expr *hook_expr_restrictive = NULL; + Expr *hook_with_check_expr_restrictive = NULL; + Expr *hook_expr_permissive = NULL; + Expr *hook_with_check_expr_permissive = NULL; List *rowsec_policies; - List *hook_policies = NIL; + List *hook_policies_restrictive = NIL; + List *hook_policies_permissive = NIL; Relation rel; Oid user_id; int sec_context; int rls_status; - bool defaultDeny = true; - bool hassublinks = false; + bool defaultDeny = false; + + /* Defaults for the return values */ + *securityQuals = NIL; + *withCheckOptions = NIL; + *hasRowSecurity = false; + *hasSubLinks = false; /* This is just to get the security context */ GetUserIdAndSecContext(&user_id, &sec_context); @@ -113,14 +130,14 @@ prepend_row_security_policies(Query* root, RangeTblEntry* rte, int rt_index) if (rte->relid < FirstNormalObjectId || rte->relkind != RELKIND_RELATION || (sec_context & SECURITY_ROW_LEVEL_DISABLED)) - return false; + return; /* Determine the state of RLS for this, pass checkAsUser explicitly */ rls_status = check_enable_rls(rte->relid, rte->checkAsUser, false); /* If there is no RLS on this table at all, nothing to do */ if (rls_status == RLS_NONE) - return false; + return; /* * RLS_NONE_ENV means we are not doing any RLS now, but that may change @@ -134,18 +151,11 @@ prepend_row_security_policies(Query* root, RangeTblEntry* rte, int rt_index) * be replanned if the environment changes (GUCs, role), but we * are not adding anything here. */ - root->hasRowSecurity = true; + *hasRowSecurity = true; - return false; + return; } - /* - * We may end up getting called multiple times for the same RTE, so check - * to make sure we aren't doing double-work. - */ - if (rte->securityQuals != NIL) - return false; - /* Grab the built-in policies which should be applied to this relation. */ rel = heap_open(rte->relid, NoLock); @@ -167,20 +177,17 @@ prepend_row_security_policies(Query* root, RangeTblEntry* rte, int rt_index) defaultDeny = true; /* Now that we have our policies, build the expressions from them. */ - process_policies(rowsec_policies, rt_index, &rowsec_expr, - &rowsec_with_check_expr, &hassublinks); + process_policies(root, rowsec_policies, rt_index, &rowsec_expr, + &rowsec_with_check_expr, hasSubLinks, OR_EXPR); /* * Also, allow extensions to add their own policies. * + * extensions can add either permissive or restrictive policies. + * * Note that, as with the internal policies, if multiple policies are * returned then they will be combined into a single expression with - * all of them OR'd together. However, to avoid the situation of an - * extension granting more access to a table than the internal policies - * would allow, the extension's policies are AND'd with the internal - * policies. In other words - extensions can only provide further - * filtering of the result set (or further reduce the set of records - * allowed to be added). + * all of them OR'd (for permissive) or AND'd (for restrictive) together. * * If only a USING policy is returned by the extension then it will be * used for WITH CHECK as well, similar to how internal policies are @@ -192,23 +199,42 @@ prepend_row_security_policies(Query* root, RangeTblEntry* rte, int rt_index) * default-deny policy and use only the policies returned by the * extension. */ - if (row_security_policy_hook) + if (row_security_policy_hook_restrictive) { - hook_policies = (*row_security_policy_hook)(root->commandType, rel); + hook_policies_restrictive = (*row_security_policy_hook_restrictive)(root->commandType, rel); /* Build the expression from any policies returned. */ - process_policies(hook_policies, rt_index, &hook_expr, - &hook_with_check_expr, &hassublinks); + if (hook_policies_restrictive != NIL) + process_policies(root, hook_policies_restrictive, rt_index, + &hook_expr_restrictive, + &hook_with_check_expr_restrictive, + hasSubLinks, + AND_EXPR); + } + + if (row_security_policy_hook_permissive) + { + hook_policies_permissive = (*row_security_policy_hook_permissive)(root->commandType, rel); + + /* Build the expression from any policies returned. */ + if (hook_policies_permissive != NIL) + process_policies(root, hook_policies_permissive, rt_index, + &hook_expr_permissive, + &hook_with_check_expr_permissive, hasSubLinks, + OR_EXPR); } /* * If the only built-in policy is the default-deny one, and hook * policies exist, then use the hook policies only and do not apply - * the default-deny policy. Otherwise, apply both sets (AND'd - * together). + * the default-deny policy. Otherwise, we will apply both sets below. */ - if (defaultDeny && hook_policies != NIL) + if (defaultDeny && + (hook_policies_restrictive != NIL || hook_policies_permissive != NIL)) + { rowsec_expr = NULL; + rowsec_with_check_expr = NULL; + } /* * For INSERT or UPDATE, we need to add the WITH CHECK quals to @@ -222,7 +248,28 @@ prepend_row_security_policies(Query* root, RangeTblEntry* rte, int rt_index) * WITH CHECK OPTIONS wants a WCO node which wraps each Expr, so * create them as necessary. */ - if (rowsec_with_check_expr) + + /* + * Handle any restrictive policies first. + * + * They can simply be added. + */ + if (hook_with_check_expr_restrictive) + { + WithCheckOption *wco; + + wco = (WithCheckOption *) makeNode(WithCheckOption); + wco->viewname = RelationGetRelationName(rel); + wco->qual = (Node *) hook_with_check_expr_restrictive; + wco->cascaded = false; + *withCheckOptions = lappend(*withCheckOptions, wco); + } + + /* + * Handle built-in policies, if there are no permissive + * policies from the hook. + */ + if (rowsec_with_check_expr && !hook_with_check_expr_permissive) { WithCheckOption *wco; @@ -230,21 +277,36 @@ prepend_row_security_policies(Query* root, RangeTblEntry* rte, int rt_index) wco->viewname = RelationGetRelationName(rel); wco->qual = (Node *) rowsec_with_check_expr; wco->cascaded = false; - root->withCheckOptions = lcons(wco, root->withCheckOptions); + *withCheckOptions = lappend(*withCheckOptions, wco); } - - /* - * Ditto for the expression, if any, returned from the extension. - */ - if (hook_with_check_expr) + /* Handle the hook policies, if there are no built-in ones. */ + else if (!rowsec_with_check_expr && hook_with_check_expr_permissive) { WithCheckOption *wco; wco = (WithCheckOption *) makeNode(WithCheckOption); wco->viewname = RelationGetRelationName(rel); - wco->qual = (Node *) hook_with_check_expr; + wco->qual = (Node *) hook_with_check_expr_permissive; wco->cascaded = false; - root->withCheckOptions = lcons(wco, root->withCheckOptions); + *withCheckOptions = lappend(*withCheckOptions, wco); + } + /* Handle the case where there are both. */ + else if (rowsec_with_check_expr && hook_with_check_expr_permissive) + { + WithCheckOption *wco; + List *combined_quals = NIL; + Expr *combined_qual_eval; + + combined_quals = lcons(copyObject(rowsec_with_check_expr), combined_quals); + combined_quals = lcons(copyObject(hook_with_check_expr_permissive), combined_quals); + + combined_qual_eval = makeBoolExpr(OR_EXPR, combined_quals, -1); + + wco = (WithCheckOption *) makeNode(WithCheckOption); + wco->viewname = RelationGetRelationName(rel); + wco->qual = (Node *) combined_qual_eval; + wco->cascaded = false; + *withCheckOptions = lappend(*withCheckOptions, wco); } } @@ -253,12 +315,29 @@ prepend_row_security_policies(Query* root, RangeTblEntry* rte, int rt_index) || root->commandType == CMD_UPDATE || root->commandType == CMD_DELETE) { - if (rowsec_expr) - rte->securityQuals = lcons(rowsec_expr, rte->securityQuals); + /* restrictive policies can simply be added to the list first */ + if (hook_expr_restrictive) + *securityQuals = lappend(*securityQuals, hook_expr_restrictive); - if (hook_expr) - rte->securityQuals = lcons(hook_expr, - rte->securityQuals); + /* If we only have internal permissive, then just add those */ + if (rowsec_expr && !hook_expr_permissive) + *securityQuals = lappend(*securityQuals, rowsec_expr); + /* .. and if we have only permissive policies from the hook */ + else if (!rowsec_expr && hook_expr_permissive) + *securityQuals = lappend(*securityQuals, hook_expr_permissive); + /* if we have both, we have to combine them with an OR */ + else if (rowsec_expr && hook_expr_permissive) + { + List *combined_quals = NIL; + Expr *combined_qual_eval; + + combined_quals = lcons(copyObject(rowsec_expr), combined_quals); + combined_quals = lcons(copyObject(hook_expr_permissive), combined_quals); + + combined_qual_eval = makeBoolExpr(OR_EXPR, combined_quals, -1); + + *securityQuals = lappend(*securityQuals, combined_qual_eval); + } } heap_close(rel, NoLock); @@ -267,17 +346,9 @@ prepend_row_security_policies(Query* root, RangeTblEntry* rte, int rt_index) * Mark this query as having row security, so plancache can invalidate * it when necessary (eg: role changes) */ - root->hasRowSecurity = true; + *hasRowSecurity = true; - /* - * If we have sublinks added because of the policies being added to the - * query, then set hasSubLinks on the Query to force subLinks to be - * properly expanded. - */ - root->hasSubLinks |= hassublinks; - - /* If we got this far, we must have added quals */ - return true; + return; } /* @@ -292,7 +363,6 @@ pull_row_security_policies(CmdType cmd, Relation relation, Oid user_id) { List *policies = NIL; ListCell *item; - RowSecurityPolicy *policy; /* * Row security is enabled for the relation and the row security GUC is @@ -302,7 +372,7 @@ pull_row_security_policies(CmdType cmd, Relation relation, Oid user_id) */ foreach(item, relation->rd_rsdesc->policies) { - policy = (RowSecurityPolicy *) lfirst(item); + RowSecurityPolicy *policy = (RowSecurityPolicy *) lfirst(item); /* Always add ALL policies, if they exist. */ if (policy->polcmd == '*' && @@ -383,8 +453,9 @@ pull_row_security_policies(CmdType cmd, Relation relation, Oid user_id) * qual_eval, with_check_eval, and hassublinks are output variables */ static void -process_policies(List *policies, int rt_index, Expr **qual_eval, - Expr **with_check_eval, bool *hassublinks) +process_policies(Query* root, List *policies, int rt_index, Expr **qual_eval, + Expr **with_check_eval, bool *hassublinks, + BoolExprType boolop) { ListCell *item; List *quals = NIL; @@ -392,7 +463,8 @@ process_policies(List *policies, int rt_index, Expr **qual_eval, /* * Extract the USING and WITH CHECK quals from each of the policies - * and add them to our lists. + * and add them to our lists. We only want WITH CHECK quals if this + * RTE is the query's result relation. */ foreach(item, policies) { @@ -401,10 +473,22 @@ process_policies(List *policies, int rt_index, Expr **qual_eval, if (policy->qual != NULL) quals = lcons(copyObject(policy->qual), quals); - if (policy->with_check_qual != NULL) + if (policy->with_check_qual != NULL && + rt_index == root->resultRelation) with_check_quals = lcons(copyObject(policy->with_check_qual), with_check_quals); + /* + * For each policy, if there is only a USING clause then copy/use it for + * the WITH CHECK policy also, if this RTE is the query's result + * relation. + */ + if (policy->qual != NULL && policy->with_check_qual == NULL && + rt_index == root->resultRelation) + with_check_quals = lcons(copyObject(policy->qual), + with_check_quals); + + if (policy->hassublinks) *hassublinks = true; } @@ -417,13 +501,6 @@ process_policies(List *policies, int rt_index, Expr **qual_eval, quals = lcons(makeConst(BOOLOID, -1, InvalidOid, sizeof(bool), BoolGetDatum(false), false, true), quals); - /* - * If we end up with only USING quals, then use those as - * WITH CHECK quals also. - */ - if (with_check_quals == NIL) - with_check_quals = copyObject(quals); - /* * Row security quals always have the target table as varno 1, as no * joins are permitted in row security expressions. We must walk the @@ -431,27 +508,40 @@ process_policies(List *policies, int rt_index, Expr **qual_eval, * the table has in the outer query. * * We rewrite the expression in-place. + * + * We must have some quals at this point; the default-deny policy, if + * nothing else. Note that we might not have any WITH CHECK quals- + * that's fine, as this might not be the resultRelation. */ + Assert(quals != NIL); + ChangeVarNodes((Node *) quals, 1, rt_index, 0); - ChangeVarNodes((Node *) with_check_quals, 1, rt_index, 0); + + if (with_check_quals != NIL) + ChangeVarNodes((Node *) with_check_quals, 1, rt_index, 0); /* * If more than one security qual is returned, then they need to be - * OR'ed together. + * combined together. */ if (list_length(quals) > 1) - *qual_eval = makeBoolExpr(OR_EXPR, quals, -1); + *qual_eval = makeBoolExpr(boolop, quals, -1); else *qual_eval = (Expr*) linitial(quals); /* - * If more than one WITH CHECK qual is returned, then they need to - * be OR'ed together. + * Similairly, if more than one WITH CHECK qual is returned, then + * they need to be combined together. + * + * with_check_quals is allowed to be NIL here since this might not be the + * resultRelation (see above). */ if (list_length(with_check_quals) > 1) - *with_check_eval = makeBoolExpr(OR_EXPR, with_check_quals, -1); - else + *with_check_eval = makeBoolExpr(boolop, with_check_quals, -1); + else if (with_check_quals != NIL) *with_check_eval = (Expr*) linitial(with_check_quals); + else + *with_check_eval = NULL; return; } diff --git a/src/include/rewrite/rowsecurity.h b/src/include/rewrite/rowsecurity.h index 8d19bfdf4a..115c9a8e43 100644 --- a/src/include/rewrite/rowsecurity.h +++ b/src/include/rewrite/rowsecurity.h @@ -37,9 +37,12 @@ typedef struct RowSecurityDesc typedef List *(*row_security_policy_hook_type)(CmdType cmdtype, Relation relation); -extern PGDLLIMPORT row_security_policy_hook_type row_security_policy_hook; +extern PGDLLIMPORT row_security_policy_hook_type row_security_policy_hook_permissive; -extern bool prepend_row_security_policies(Query* root, RangeTblEntry* rte, - int rt_index); +extern PGDLLIMPORT row_security_policy_hook_type row_security_policy_hook_restrictive; + +extern void get_row_security_policies(Query* root, RangeTblEntry* rte, int rt_index, + List **securityQuals, List **withCheckOptions, + bool *hasRowSecurity, bool *hasSubLinks); #endif /* ROWSECURITY_H */ diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile index 93d93af2cd..730fa75a35 100644 --- a/src/test/modules/Makefile +++ b/src/test/modules/Makefile @@ -8,6 +8,7 @@ SUBDIRS = \ commit_ts \ worker_spi \ dummy_seclabel \ + test_rls_hooks \ test_shm_mq \ test_parser diff --git a/src/test/modules/test_rls_hooks/.gitignore b/src/test/modules/test_rls_hooks/.gitignore new file mode 100644 index 0000000000..5dcb3ff972 --- /dev/null +++ b/src/test/modules/test_rls_hooks/.gitignore @@ -0,0 +1,4 @@ +# Generated subdirectories +/log/ +/results/ +/tmp_check/ diff --git a/src/test/modules/test_rls_hooks/Makefile b/src/test/modules/test_rls_hooks/Makefile new file mode 100644 index 0000000000..6b772c4db1 --- /dev/null +++ b/src/test/modules/test_rls_hooks/Makefile @@ -0,0 +1,22 @@ +# src/test/modules/test_rls_hooks/Makefile + +MODULE_big = test_rls_hooks +OBJS = test_rls_hooks.o $(WIN32RES) +PGFILEDESC = "test_rls_hooks - example use of RLS hooks" + +EXTENSION = test_rls_hooks +# DATA = test_rls_hooks--1.0.sql + +REGRESS = test_rls_hooks +REGRESS_OPTS = --temp-config=$(top_srcdir)/src/test/modules/test_rls_hooks/rls_hooks.conf + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_rls_hooks +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/test_rls_hooks/README b/src/test/modules/test_rls_hooks/README new file mode 100644 index 0000000000..b61dace572 --- /dev/null +++ b/src/test/modules/test_rls_hooks/README @@ -0,0 +1,16 @@ +test_rls_hooks is an example of how to use the hooks provided for RLS to +define additional policies to be used. + +Functions +========= +test_rls_hook_permissive(CmdType cmdtype, Relation relation) + RETURNS List* + +Returns a list of policies which should be added to any existing +policies on the relation, combined with OR. + +test_rls_hook_restrictive(CmdType cmdtype, Relation relation) + RETURNS List* + +Returns a list of policies which should be added to any existing +policies on the relation, combined with AND. diff --git a/src/test/modules/test_rls_hooks/expected/test_rls_hooks.out b/src/test/modules/test_rls_hooks/expected/test_rls_hooks.out new file mode 100644 index 0000000000..d55aea726e --- /dev/null +++ b/src/test/modules/test_rls_hooks/expected/test_rls_hooks.out @@ -0,0 +1,193 @@ +CREATE TABLE rls_test_permissive ( + username name, + supervisor name, + data integer +); +-- initial test data +INSERT INTO rls_test_permissive VALUES ('r1','s1',4); +INSERT INTO rls_test_permissive VALUES ('r2','s2',5); +INSERT INTO rls_test_permissive VALUES ('r3','s3',6); +CREATE TABLE rls_test_restrictive ( + username name, + supervisor name, + data integer +); +-- initial test data +INSERT INTO rls_test_restrictive VALUES ('r1','s1',1); +INSERT INTO rls_test_restrictive VALUES ('r2','s2',2); +INSERT INTO rls_test_restrictive VALUES ('r3','s3',3); +CREATE TABLE rls_test_both ( + username name, + supervisor name, + data integer +); +-- initial test data +INSERT INTO rls_test_both VALUES ('r1','s1',7); +INSERT INTO rls_test_both VALUES ('r2','s2',8); +INSERT INTO rls_test_both VALUES ('r3','s3',9); +ALTER TABLE rls_test_permissive ENABLE ROW LEVEL SECURITY; +ALTER TABLE rls_test_restrictive ENABLE ROW LEVEL SECURITY; +ALTER TABLE rls_test_both ENABLE ROW LEVEL SECURITY; +CREATE ROLE r1; +CREATE ROLE s1; +GRANT SELECT,INSERT ON rls_test_permissive TO r1; +GRANT SELECT,INSERT ON rls_test_restrictive TO r1; +GRANT SELECT,INSERT ON rls_test_both TO r1; +GRANT SELECT,INSERT ON rls_test_permissive TO s1; +GRANT SELECT,INSERT ON rls_test_restrictive TO s1; +GRANT SELECT,INSERT ON rls_test_both TO s1; +SET ROLE r1; +-- With only the hook's policies, permissive +-- hook's policy is current_user = username +EXPLAIN (costs off) SELECT * FROM rls_test_permissive; + QUERY PLAN +----------------------------------------- + Seq Scan on rls_test_permissive + Filter: ("current_user"() = username) +(2 rows) + +SELECT * FROM rls_test_permissive; + username | supervisor | data +----------+------------+------ + r1 | s1 | 4 +(1 row) + +-- success +INSERT INTO rls_test_permissive VALUES ('r1','s1',10); +-- failure +INSERT INTO rls_test_permissive VALUES ('r4','s4',10); +ERROR: new row violates WITH CHECK OPTION for "rls_test_permissive" +SET ROLE s1; +-- With only the hook's policies, restrictive +-- hook's policy is current_user = supervisor +EXPLAIN (costs off) SELECT * FROM rls_test_restrictive; + QUERY PLAN +------------------------------------------- + Seq Scan on rls_test_restrictive + Filter: ("current_user"() = supervisor) +(2 rows) + +SELECT * FROM rls_test_restrictive; + username | supervisor | data +----------+------------+------ + r1 | s1 | 1 +(1 row) + +-- success +INSERT INTO rls_test_restrictive VALUES ('r1','s1',10); +-- failure +INSERT INTO rls_test_restrictive VALUES ('r4','s4',10); +ERROR: new row violates WITH CHECK OPTION for "rls_test_restrictive" +SET ROLE s1; +-- With only the hook's policies, both +-- permissive hook's policy is current_user = username +-- restrictive hook's policy is current_user = superuser +-- combined with AND, results in nothing being allowed +EXPLAIN (costs off) SELECT * FROM rls_test_both; + QUERY PLAN +------------------------------------------------------- + Subquery Scan on rls_test_both + Filter: ("current_user"() = rls_test_both.username) + -> Seq Scan on rls_test_both rls_test_both_1 + Filter: ("current_user"() = supervisor) +(4 rows) + +SELECT * FROM rls_test_both; + username | supervisor | data +----------+------------+------ +(0 rows) + +-- failure +INSERT INTO rls_test_both VALUES ('r1','s1',10); +ERROR: new row violates WITH CHECK OPTION for "rls_test_both" +-- failure +INSERT INTO rls_test_both VALUES ('r4','s1',10); +ERROR: new row violates WITH CHECK OPTION for "rls_test_both" +-- failure +INSERT INTO rls_test_both VALUES ('r4','s4',10); +ERROR: new row violates WITH CHECK OPTION for "rls_test_both" +RESET ROLE; +-- Create "internal" policies, to check that the policies from +-- the hooks are combined correctly. +CREATE POLICY p1 ON rls_test_permissive USING (data % 2 = 0); +CREATE POLICY p1 ON rls_test_restrictive USING (data % 2 = 0); +CREATE POLICY p1 ON rls_test_both USING (data % 2 = 0); +SET ROLE r1; +-- With both internal and hook policies, permissive +EXPLAIN (costs off) SELECT * FROM rls_test_permissive; + QUERY PLAN +--------------------------------------------------------------- + Seq Scan on rls_test_permissive + Filter: (("current_user"() = username) OR ((data % 2) = 0)) +(2 rows) + +SELECT * FROM rls_test_permissive; + username | supervisor | data +----------+------------+------ + r1 | s1 | 4 + r3 | s3 | 6 + r1 | s1 | 10 +(3 rows) + +-- success +INSERT INTO rls_test_permissive VALUES ('r1','s1',7); +-- success +INSERT INTO rls_test_permissive VALUES ('r3','s3',10); +-- failure +INSERT INTO rls_test_permissive VALUES ('r4','s4',7); +ERROR: new row violates WITH CHECK OPTION for "rls_test_permissive" +SET ROLE s1; +-- With both internal and hook policies, restrictive +EXPLAIN (costs off) SELECT * FROM rls_test_restrictive; + QUERY PLAN +--------------------------------------------------------------- + Subquery Scan on rls_test_restrictive + Filter: ((rls_test_restrictive.data % 2) = 0) + -> Seq Scan on rls_test_restrictive rls_test_restrictive_1 + Filter: ("current_user"() = supervisor) +(4 rows) + +SELECT * FROM rls_test_restrictive; + username | supervisor | data +----------+------------+------ + r1 | s1 | 10 +(1 row) + +-- success +INSERT INTO rls_test_restrictive VALUES ('r1','s1',8); +-- failure +INSERT INTO rls_test_restrictive VALUES ('r3','s3',10); +ERROR: new row violates WITH CHECK OPTION for "rls_test_restrictive" +-- failure +INSERT INTO rls_test_restrictive VALUES ('r1','s1',7); +ERROR: new row violates WITH CHECK OPTION for "rls_test_restrictive" +-- failure +INSERT INTO rls_test_restrictive VALUES ('r4','s4',7); +ERROR: new row violates WITH CHECK OPTION for "rls_test_restrictive" +-- With both internal and hook policies, both permissive +-- and restrictive hook policies +EXPLAIN (costs off) SELECT * FROM rls_test_both; + QUERY PLAN +------------------------------------------------------------------------------------------- + Subquery Scan on rls_test_both + Filter: (("current_user"() = rls_test_both.username) OR ((rls_test_both.data % 2) = 0)) + -> Seq Scan on rls_test_both rls_test_both_1 + Filter: ("current_user"() = supervisor) +(4 rows) + +SELECT * FROM rls_test_both; + username | supervisor | data +----------+------------+------ +(0 rows) + +-- success +INSERT INTO rls_test_both VALUES ('r1','s1',8); +-- failure +INSERT INTO rls_test_both VALUES ('r3','s3',10); +ERROR: new row violates WITH CHECK OPTION for "rls_test_both" +-- failure +INSERT INTO rls_test_both VALUES ('r1','s1',7); +ERROR: new row violates WITH CHECK OPTION for "rls_test_both" +-- failure +INSERT INTO rls_test_both VALUES ('r4','s4',7); +ERROR: new row violates WITH CHECK OPTION for "rls_test_both" diff --git a/src/test/modules/test_rls_hooks/rls_hooks.conf b/src/test/modules/test_rls_hooks/rls_hooks.conf new file mode 100644 index 0000000000..a522c0e550 --- /dev/null +++ b/src/test/modules/test_rls_hooks/rls_hooks.conf @@ -0,0 +1 @@ +shared_preload_libraries = test_rls_hooks diff --git a/src/test/modules/test_rls_hooks/sql/test_rls_hooks.sql b/src/test/modules/test_rls_hooks/sql/test_rls_hooks.sql new file mode 100644 index 0000000000..be13ab4c92 --- /dev/null +++ b/src/test/modules/test_rls_hooks/sql/test_rls_hooks.sql @@ -0,0 +1,157 @@ +CREATE TABLE rls_test_permissive ( + username name, + supervisor name, + data integer +); + +-- initial test data +INSERT INTO rls_test_permissive VALUES ('r1','s1',4); +INSERT INTO rls_test_permissive VALUES ('r2','s2',5); +INSERT INTO rls_test_permissive VALUES ('r3','s3',6); + +CREATE TABLE rls_test_restrictive ( + username name, + supervisor name, + data integer +); + +-- initial test data +INSERT INTO rls_test_restrictive VALUES ('r1','s1',1); +INSERT INTO rls_test_restrictive VALUES ('r2','s2',2); +INSERT INTO rls_test_restrictive VALUES ('r3','s3',3); + +CREATE TABLE rls_test_both ( + username name, + supervisor name, + data integer +); + +-- initial test data +INSERT INTO rls_test_both VALUES ('r1','s1',7); +INSERT INTO rls_test_both VALUES ('r2','s2',8); +INSERT INTO rls_test_both VALUES ('r3','s3',9); + +ALTER TABLE rls_test_permissive ENABLE ROW LEVEL SECURITY; +ALTER TABLE rls_test_restrictive ENABLE ROW LEVEL SECURITY; +ALTER TABLE rls_test_both ENABLE ROW LEVEL SECURITY; + +CREATE ROLE r1; +CREATE ROLE s1; + +GRANT SELECT,INSERT ON rls_test_permissive TO r1; +GRANT SELECT,INSERT ON rls_test_restrictive TO r1; +GRANT SELECT,INSERT ON rls_test_both TO r1; + +GRANT SELECT,INSERT ON rls_test_permissive TO s1; +GRANT SELECT,INSERT ON rls_test_restrictive TO s1; +GRANT SELECT,INSERT ON rls_test_both TO s1; + +SET ROLE r1; + +-- With only the hook's policies, permissive +-- hook's policy is current_user = username +EXPLAIN (costs off) SELECT * FROM rls_test_permissive; + +SELECT * FROM rls_test_permissive; + +-- success +INSERT INTO rls_test_permissive VALUES ('r1','s1',10); + +-- failure +INSERT INTO rls_test_permissive VALUES ('r4','s4',10); + +SET ROLE s1; + +-- With only the hook's policies, restrictive +-- hook's policy is current_user = supervisor +EXPLAIN (costs off) SELECT * FROM rls_test_restrictive; + +SELECT * FROM rls_test_restrictive; + +-- success +INSERT INTO rls_test_restrictive VALUES ('r1','s1',10); + +-- failure +INSERT INTO rls_test_restrictive VALUES ('r4','s4',10); + +SET ROLE s1; + +-- With only the hook's policies, both +-- permissive hook's policy is current_user = username +-- restrictive hook's policy is current_user = superuser +-- combined with AND, results in nothing being allowed +EXPLAIN (costs off) SELECT * FROM rls_test_both; + +SELECT * FROM rls_test_both; + +-- failure +INSERT INTO rls_test_both VALUES ('r1','s1',10); + +-- failure +INSERT INTO rls_test_both VALUES ('r4','s1',10); + +-- failure +INSERT INTO rls_test_both VALUES ('r4','s4',10); + +RESET ROLE; + +-- Create "internal" policies, to check that the policies from +-- the hooks are combined correctly. +CREATE POLICY p1 ON rls_test_permissive USING (data % 2 = 0); + +CREATE POLICY p1 ON rls_test_restrictive USING (data % 2 = 0); + +CREATE POLICY p1 ON rls_test_both USING (data % 2 = 0); + +SET ROLE r1; + +-- With both internal and hook policies, permissive +EXPLAIN (costs off) SELECT * FROM rls_test_permissive; + +SELECT * FROM rls_test_permissive; + +-- success +INSERT INTO rls_test_permissive VALUES ('r1','s1',7); + +-- success +INSERT INTO rls_test_permissive VALUES ('r3','s3',10); + +-- failure +INSERT INTO rls_test_permissive VALUES ('r4','s4',7); + +SET ROLE s1; + +-- With both internal and hook policies, restrictive +EXPLAIN (costs off) SELECT * FROM rls_test_restrictive; + +SELECT * FROM rls_test_restrictive; + +-- success +INSERT INTO rls_test_restrictive VALUES ('r1','s1',8); + +-- failure +INSERT INTO rls_test_restrictive VALUES ('r3','s3',10); + +-- failure +INSERT INTO rls_test_restrictive VALUES ('r1','s1',7); + +-- failure +INSERT INTO rls_test_restrictive VALUES ('r4','s4',7); + +-- With both internal and hook policies, both permissive +-- and restrictive hook policies +EXPLAIN (costs off) SELECT * FROM rls_test_both; + +SELECT * FROM rls_test_both; + +-- success +INSERT INTO rls_test_both VALUES ('r1','s1',8); + +-- failure +INSERT INTO rls_test_both VALUES ('r3','s3',10); + +-- failure +INSERT INTO rls_test_both VALUES ('r1','s1',7); + +-- failure +INSERT INTO rls_test_both VALUES ('r4','s4',7); diff --git a/src/test/modules/test_rls_hooks/test_rls_hooks.c b/src/test/modules/test_rls_hooks/test_rls_hooks.c new file mode 100644 index 0000000000..f2f87c351a --- /dev/null +++ b/src/test/modules/test_rls_hooks/test_rls_hooks.c @@ -0,0 +1,170 @@ +/*-------------------------------------------------------------------------- + * + * test.c + * Test harness code for shared memory message queues. + * + * Copyright (C) 2015, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_rls_hooks/test_rls_hooks.c + * + * ------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "fmgr.h" +#include "miscadmin.h" + +#include "test_rls_hooks.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +PG_MODULE_MAGIC; + +/* Saved hook values in case of unload */ +static row_security_policy_hook_type prev_row_security_policy_hook_permissive = NULL; +static row_security_policy_hook_type prev_row_security_policy_hook_restrictive = NULL; + +void _PG_init(void); +void _PG_fini(void); + +/* Install hooks */ +void _PG_init(void) +{ + /* Save values for unload */ + prev_row_security_policy_hook_permissive = row_security_policy_hook_permissive; + prev_row_security_policy_hook_restrictive = row_security_policy_hook_restrictive; + + /* Set our hooks */ + row_security_policy_hook_permissive = test_rls_hooks_permissive; + row_security_policy_hook_restrictive = test_rls_hooks_restrictive; +} + +/* Uninstall hooks */ +void _PG_fini(void) +{ + row_security_policy_hook_permissive = prev_row_security_policy_hook_permissive; + row_security_policy_hook_restrictive = prev_row_security_policy_hook_restrictive; +} + +/* + * Return permissive policies to be added + */ +List* +test_rls_hooks_permissive(CmdType cmdtype, Relation relation) +{ + List *policies = NIL; + RowSecurityPolicy *policy = palloc0(sizeof(RowSecurityPolicy)); + Datum role; + FuncCall *n; + Node *e; + ColumnRef *c; + ParseState *qual_pstate; + RangeTblEntry *rte; + + if (strcmp(RelationGetRelationName(relation),"rls_test_permissive") + && strcmp(RelationGetRelationName(relation),"rls_test_both")) + return NIL; + + qual_pstate = make_parsestate(NULL); + + rte = addRangeTableEntryForRelation(qual_pstate, relation, NULL, false, + false); + addRTEtoQuery(qual_pstate, rte, false, true, true); + + role = ObjectIdGetDatum(ACL_ID_PUBLIC); + + policy->policy_name = pstrdup("extension policy"); + policy->policy_id = InvalidOid; + policy->polcmd = '*'; + policy->roles = construct_array(&role, 1, OIDOID, sizeof(Oid), true, 'i'); + /* + policy->qual = (Expr *) makeConst(BOOLOID, -1, InvalidOid, + sizeof(bool), BoolGetDatum(true), + false, true); + */ + + n = makeFuncCall(list_make2(makeString("pg_catalog"), + makeString("current_user")), NIL, 0); + + c = makeNode(ColumnRef); + c->fields = list_make1(makeString("username")); + c->location = 0; + + e = (Node*) makeSimpleA_Expr(AEXPR_OP, "=", (Node*) n, (Node*) c, 0); + + policy->qual = (Expr*) transformWhereClause(qual_pstate, copyObject(e), + EXPR_KIND_WHERE, + "POLICY"); + + policy->with_check_qual = copyObject(policy->qual); + policy->hassublinks = false; + + policies = list_make1(policy); + + return policies; +} + +/* + * Return restrictive policies to be added + */ +List* +test_rls_hooks_restrictive(CmdType cmdtype, Relation relation) +{ + List *policies = NIL; + RowSecurityPolicy *policy = palloc0(sizeof(RowSecurityPolicy)); + Datum role; + FuncCall *n; + Node *e; + ColumnRef *c; + ParseState *qual_pstate; + RangeTblEntry *rte; + + + if (strcmp(RelationGetRelationName(relation),"rls_test_restrictive") + && strcmp(RelationGetRelationName(relation),"rls_test_both")) + return NIL; + + qual_pstate = make_parsestate(NULL); + + rte = addRangeTableEntryForRelation(qual_pstate, relation, NULL, false, + false); + addRTEtoQuery(qual_pstate, rte, false, true, true); + + role = ObjectIdGetDatum(ACL_ID_PUBLIC); + + policy->policy_name = pstrdup("extension policy"); + policy->policy_id = InvalidOid; + policy->polcmd = '*'; + policy->roles = construct_array(&role, 1, OIDOID, sizeof(Oid), true, 'i'); + + n = makeFuncCall(list_make2(makeString("pg_catalog"), + makeString("current_user")), NIL, 0); + + c = makeNode(ColumnRef); + c->fields = list_make1(makeString("supervisor")); + c->location = 0; + + e = (Node*) makeSimpleA_Expr(AEXPR_OP, "=", (Node*) n, (Node*) c, 0); + + policy->qual = (Expr*) transformWhereClause(qual_pstate, copyObject(e), + EXPR_KIND_WHERE, + "POLICY"); + + policy->with_check_qual = copyObject(policy->qual); + policy->hassublinks = false; + + policies = list_make1(policy); + + return policies; +} diff --git a/src/test/modules/test_rls_hooks/test_rls_hooks.control b/src/test/modules/test_rls_hooks/test_rls_hooks.control new file mode 100644 index 0000000000..9f9f13f76c --- /dev/null +++ b/src/test/modules/test_rls_hooks/test_rls_hooks.control @@ -0,0 +1,4 @@ +comment = 'Test code for RLS hooks' +default_version = '1.0' +module_pathname = '$libdir/test_rls_hooks' +relocatable = true diff --git a/src/test/modules/test_rls_hooks/test_rls_hooks.h b/src/test/modules/test_rls_hooks/test_rls_hooks.h new file mode 100644 index 0000000000..c8a0330602 --- /dev/null +++ b/src/test/modules/test_rls_hooks/test_rls_hooks.h @@ -0,0 +1,25 @@ +/*-------------------------------------------------------------------------- + * + * test_rls_hooks.h + * Definitions for RLS hooks + * + * Copyright (C) 2015, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_rls_hooks/test_rls_hooks.h + * + * ------------------------------------------------------------------------- + */ + +#ifndef TEST_RLS_HOOKS_H +#define TEST_RLS_HOOKS_H + +#include + +/* Return set of permissive hooks based on CmdType and Relation */ +extern List *test_rls_hooks_permissive(CmdType cmdtype, Relation relation); + +/* Return set of restrictive hooks based on CmdType and Relation */ +extern List *test_rls_hooks_restrictive(CmdType cmdtype, Relation relation); + +#endif diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out index 44e8dab44c..56760796ed 100644 --- a/src/test/regress/expected/rowsecurity.out +++ b/src/test/regress/expected/rowsecurity.out @@ -466,9 +466,11 @@ ALTER TABLE t1 DROP COLUMN junk1; -- just a disturbing factor GRANT ALL ON t1 TO public; COPY t1 FROM stdin WITH (oids); CREATE TABLE t2 (c float) INHERITS (t1); +GRANT ALL ON t2 TO public; COPY t2 FROM stdin WITH (oids); CREATE TABLE t3 (c text, b text, a int) WITH OIDS; ALTER TABLE t3 INHERIT t1; +GRANT ALL ON t3 TO public; COPY t3(a,b,c) FROM stdin WITH (oids); CREATE POLICY p1 ON t1 FOR ALL TO PUBLIC USING (a % 2 = 0); -- be even number CREATE POLICY p2 ON t2 FOR ALL TO PUBLIC USING (a % 2 = 1); -- be odd number @@ -1117,22 +1119,216 @@ NOTICE: f_leak => yyyyyy 302 | 2 | yyyyyy | (2,yyyyyy) (5 rows) +-- updates with from clause +EXPLAIN (COSTS OFF) UPDATE t2 SET b=t2.b FROM t3 +WHERE t2.a = 3 and t3.a = 2 AND f_leak(t2.b) AND f_leak(t3.b); + QUERY PLAN +--------------------------------------------------------------- + Update on t2 t2_1 + -> Nested Loop + -> Subquery Scan on t2 + Filter: f_leak(t2.b) + -> LockRows + -> Seq Scan on t2 t2_2 + Filter: ((a = 3) AND ((a % 2) = 1)) + -> Seq Scan on t3 + Filter: (f_leak(b) AND (a = 2)) +(9 rows) + +UPDATE t2 SET b=t2.b FROM t3 +WHERE t2.a = 3 and t3.a = 2 AND f_leak(t2.b) AND f_leak(t3.b); +NOTICE: f_leak => cde +NOTICE: f_leak => xxx +NOTICE: f_leak => zzz +NOTICE: f_leak => yyyyyy +EXPLAIN (COSTS OFF) UPDATE t1 SET b=t1.b FROM t2 +WHERE t1.a = 3 and t2.a = 3 AND f_leak(t1.b) AND f_leak(t2.b); + QUERY PLAN +--------------------------------------------------------------- + Update on t1 t1_3 + Update on t1 t1_3 + Update on t2 t1 + Update on t3 t1 + -> Nested Loop + -> Subquery Scan on t1 + Filter: f_leak(t1.b) + -> LockRows + -> Seq Scan on t1 t1_4 + Filter: ((a = 3) AND ((a % 2) = 0)) + -> Subquery Scan on t2 + Filter: f_leak(t2.b) + -> Seq Scan on t2 t2_3 + Filter: ((a = 3) AND ((a % 2) = 1)) + -> Nested Loop + -> Subquery Scan on t1_1 + Filter: f_leak(t1_1.b) + -> LockRows + -> Seq Scan on t2 t2_4 + Filter: ((a = 3) AND ((a % 2) = 0)) + -> Subquery Scan on t2_1 + Filter: f_leak(t2_1.b) + -> Seq Scan on t2 t2_5 + Filter: ((a = 3) AND ((a % 2) = 1)) + -> Nested Loop + -> Subquery Scan on t1_2 + Filter: f_leak(t1_2.b) + -> LockRows + -> Seq Scan on t3 + Filter: ((a = 3) AND ((a % 2) = 0)) + -> Subquery Scan on t2_2 + Filter: f_leak(t2_2.b) + -> Seq Scan on t2 t2_6 + Filter: ((a = 3) AND ((a % 2) = 1)) +(34 rows) + +UPDATE t1 SET b=t1.b FROM t2 +WHERE t1.a = 3 and t2.a = 3 AND f_leak(t1.b) AND f_leak(t2.b); +EXPLAIN (COSTS OFF) UPDATE t2 SET b=t2.b FROM t1 +WHERE t1.a = 3 and t2.a = 3 AND f_leak(t1.b) AND f_leak(t2.b); + QUERY PLAN +--------------------------------------------------------------------- + Update on t2 t2_1 + -> Nested Loop + -> Subquery Scan on t2 + Filter: f_leak(t2.b) + -> LockRows + -> Seq Scan on t2 t2_2 + Filter: ((a = 3) AND ((a % 2) = 1)) + -> Subquery Scan on t1 + Filter: f_leak(t1.b) + -> Result + -> Append + -> Seq Scan on t1 t1_1 + Filter: ((a = 3) AND ((a % 2) = 0)) + -> Seq Scan on t2 t2_3 + Filter: ((a = 3) AND ((a % 2) = 0)) + -> Seq Scan on t3 + Filter: ((a = 3) AND ((a % 2) = 0)) +(17 rows) + +UPDATE t2 SET b=t2.b FROM t1 +WHERE t1.a = 3 and t2.a = 3 AND f_leak(t1.b) AND f_leak(t2.b); +NOTICE: f_leak => cde +-- updates with from clause self join +EXPLAIN (COSTS OFF) UPDATE t2 t2_1 SET b = t2_2.b FROM t2 t2_2 +WHERE t2_1.a = 3 AND t2_2.a = t2_1.a AND t2_2.b = t2_1.b +AND f_leak(t2_1.b) AND f_leak(t2_2.b) RETURNING *, t2_1, t2_2; + QUERY PLAN +--------------------------------------------------------------- + Update on t2 t2_1_1 + -> Nested Loop + Join Filter: (t2_1.b = t2_2.b) + -> Subquery Scan on t2_1 + Filter: f_leak(t2_1.b) + -> LockRows + -> Seq Scan on t2 t2_1_2 + Filter: ((a = 3) AND ((a % 2) = 1)) + -> Subquery Scan on t2_2 + Filter: f_leak(t2_2.b) + -> Seq Scan on t2 t2_2_1 + Filter: ((a = 3) AND ((a % 2) = 1)) +(12 rows) + +UPDATE t2 t2_1 SET b = t2_2.b FROM t2 t2_2 +WHERE t2_1.a = 3 AND t2_2.a = t2_1.a AND t2_2.b = t2_1.b +AND f_leak(t2_1.b) AND f_leak(t2_2.b) RETURNING *, t2_1, t2_2; +NOTICE: f_leak => cde +NOTICE: f_leak => cde + a | b | c | a | b | c | t2_1 | t2_2 +---+-----+-----+---+-----+-----+-------------+------------- + 3 | cde | 3.3 | 3 | cde | 3.3 | (3,cde,3.3) | (3,cde,3.3) +(1 row) + +EXPLAIN (COSTS OFF) UPDATE t1 t1_1 SET b = t1_2.b FROM t1 t1_2 +WHERE t1_1.a = 4 AND t1_2.a = t1_1.a AND t1_2.b = t1_1.b +AND f_leak(t1_1.b) AND f_leak(t1_2.b) RETURNING *, t1_1, t1_2; + QUERY PLAN +--------------------------------------------------------------- + Update on t1 t1_1_3 + Update on t1 t1_1_3 + Update on t2 t1_1 + Update on t3 t1_1 + -> Nested Loop + Join Filter: (t1_1.b = t1_2.b) + -> Subquery Scan on t1_1 + Filter: f_leak(t1_1.b) + -> LockRows + -> Seq Scan on t1 t1_1_4 + Filter: ((a = 4) AND ((a % 2) = 0)) + -> Subquery Scan on t1_2 + Filter: f_leak(t1_2.b) + -> Append + -> Seq Scan on t1 t1_2_3 + Filter: ((a = 4) AND ((a % 2) = 0)) + -> Seq Scan on t2 t1_2_4 + Filter: ((a = 4) AND ((a % 2) = 0)) + -> Seq Scan on t3 t1_2_5 + Filter: ((a = 4) AND ((a % 2) = 0)) + -> Nested Loop + Join Filter: (t1_1_1.b = t1_2_1.b) + -> Subquery Scan on t1_1_1 + Filter: f_leak(t1_1_1.b) + -> LockRows + -> Seq Scan on t2 t1_1_5 + Filter: ((a = 4) AND ((a % 2) = 0)) + -> Subquery Scan on t1_2_1 + Filter: f_leak(t1_2_1.b) + -> Append + -> Seq Scan on t1 t1_2_6 + Filter: ((a = 4) AND ((a % 2) = 0)) + -> Seq Scan on t2 t1_2_7 + Filter: ((a = 4) AND ((a % 2) = 0)) + -> Seq Scan on t3 t1_2_8 + Filter: ((a = 4) AND ((a % 2) = 0)) + -> Nested Loop + Join Filter: (t1_1_2.b = t1_2_2.b) + -> Subquery Scan on t1_1_2 + Filter: f_leak(t1_1_2.b) + -> LockRows + -> Seq Scan on t3 t1_1_6 + Filter: ((a = 4) AND ((a % 2) = 0)) + -> Subquery Scan on t1_2_2 + Filter: f_leak(t1_2_2.b) + -> Append + -> Seq Scan on t1 t1_2_9 + Filter: ((a = 4) AND ((a % 2) = 0)) + -> Seq Scan on t2 t1_2_10 + Filter: ((a = 4) AND ((a % 2) = 0)) + -> Seq Scan on t3 t1_2_11 + Filter: ((a = 4) AND ((a % 2) = 0)) +(52 rows) + +UPDATE t1 t1_1 SET b = t1_2.b FROM t1 t1_2 +WHERE t1_1.a = 4 AND t1_2.a = t1_1.a AND t1_2.b = t1_1.b +AND f_leak(t1_1.b) AND f_leak(t1_2.b) RETURNING *, t1_1, t1_2; +NOTICE: f_leak => dddddd_updt +NOTICE: f_leak => dddddd_updt +NOTICE: f_leak => defdef +NOTICE: f_leak => defdef +NOTICE: f_leak => dddddd_updt +NOTICE: f_leak => defdef + a | b | a | b | t1_1 | t1_2 +---+-------------+---+-------------+-----------------+----------------- + 4 | dddddd_updt | 4 | dddddd_updt | (4,dddddd_updt) | (4,dddddd_updt) + 4 | defdef | 4 | defdef | (4,defdef) | (4,defdef) +(2 rows) + RESET SESSION AUTHORIZATION; SET row_security TO OFF; -SELECT * FROM t1; +SELECT * FROM t1 ORDER BY a,b; a | b ---+------------- 1 | aaa - 3 | ccc - 2 | bbbbbb_updt - 4 | dddddd_updt 1 | abc - 3 | cde - 2 | bcdbcd - 4 | defdef 1 | xxx - 3 | zzz + 2 | bbbbbb_updt + 2 | bcdbcd 2 | yyyyyy + 3 | ccc + 3 | cde + 3 | zzz + 4 | dddddd_updt + 4 | defdef (11 rows) SET SESSION AUTHORIZATION rls_regress_user1; @@ -1192,6 +1388,103 @@ NOTICE: f_leak => yyyyyy 302 | 2 | yyyyyy | (2,yyyyyy) (3 rows) +-- +-- S.b. view on top of Row-level security +-- +SET SESSION AUTHORIZATION rls_regress_user0; +CREATE TABLE b1 (a int, b text); +INSERT INTO b1 (SELECT x, md5(x::text) FROM generate_series(-10,10) x); +CREATE POLICY p1 ON b1 USING (a % 2 = 0); +ALTER TABLE b1 ENABLE ROW LEVEL SECURITY; +GRANT ALL ON b1 TO rls_regress_user1; +SET SESSION AUTHORIZATION rls_regress_user1; +CREATE VIEW bv1 WITH (security_barrier) AS SELECT * FROM b1 WHERE a > 0 WITH CHECK OPTION; +GRANT ALL ON bv1 TO rls_regress_user2; +SET SESSION AUTHORIZATION rls_regress_user2; +EXPLAIN (COSTS OFF) SELECT * FROM bv1 WHERE f_leak(b); + QUERY PLAN +--------------------------------------------- + Subquery Scan on bv1 + Filter: f_leak(bv1.b) + -> Seq Scan on b1 + Filter: ((a > 0) AND ((a % 2) = 0)) +(4 rows) + +SELECT * FROM bv1 WHERE f_leak(b); +NOTICE: f_leak => c81e728d9d4c2f636f067f89cc14862c +NOTICE: f_leak => a87ff679a2f3e71d9181a67b7542122c +NOTICE: f_leak => 1679091c5a880faf6fb5e6087eb1b2dc +NOTICE: f_leak => c9f0f895fb98ab9159f51fd0297e236d +NOTICE: f_leak => d3d9446802a44259755d38e6d163e820 + a | b +----+---------------------------------- + 2 | c81e728d9d4c2f636f067f89cc14862c + 4 | a87ff679a2f3e71d9181a67b7542122c + 6 | 1679091c5a880faf6fb5e6087eb1b2dc + 8 | c9f0f895fb98ab9159f51fd0297e236d + 10 | d3d9446802a44259755d38e6d163e820 +(5 rows) + +INSERT INTO bv1 VALUES (-1, 'xxx'); -- should fail view WCO +ERROR: new row violates WITH CHECK OPTION for "b1" +INSERT INTO bv1 VALUES (11, 'xxx'); -- should fail RLS check +ERROR: new row violates WITH CHECK OPTION for "b1" +INSERT INTO bv1 VALUES (12, 'xxx'); -- ok +EXPLAIN (COSTS OFF) UPDATE bv1 SET b = 'yyy' WHERE a = 4 AND f_leak(b); + QUERY PLAN +--------------------------------------------------------------------------- + Update on b1 b1_1 + -> Subquery Scan on b1 + Filter: f_leak(b1.b) + -> Subquery Scan on b1_2 + -> LockRows + -> Seq Scan on b1 b1_3 + Filter: ((a > 0) AND (a = 4) AND ((a % 2) = 0)) +(7 rows) + +UPDATE bv1 SET b = 'yyy' WHERE a = 4 AND f_leak(b); +NOTICE: f_leak => a87ff679a2f3e71d9181a67b7542122c +EXPLAIN (COSTS OFF) DELETE FROM bv1 WHERE a = 6 AND f_leak(b); + QUERY PLAN +--------------------------------------------------------------------------- + Delete on b1 b1_1 + -> Subquery Scan on b1 + Filter: f_leak(b1.b) + -> Subquery Scan on b1_2 + -> LockRows + -> Seq Scan on b1 b1_3 + Filter: ((a > 0) AND (a = 6) AND ((a % 2) = 0)) +(7 rows) + +DELETE FROM bv1 WHERE a = 6 AND f_leak(b); +NOTICE: f_leak => 1679091c5a880faf6fb5e6087eb1b2dc +SET SESSION AUTHORIZATION rls_regress_user0; +SELECT * FROM b1; + a | b +-----+---------------------------------- + -10 | 1b0fd9efa5279c4203b7c70233f86dbf + -9 | 252e691406782824eec43d7eadc3d256 + -8 | a8d2ec85eaf98407310b72eb73dda247 + -7 | 74687a12d3915d3c4d83f1af7b3683d5 + -6 | 596a3d04481816330f07e4f97510c28f + -5 | 47c1b025fa18ea96c33fbb6718688c0f + -4 | 0267aaf632e87a63288a08331f22c7c3 + -3 | b3149ecea4628efd23d2f86e5a723472 + -2 | 5d7b9adcbe1c629ec722529dd12e5129 + -1 | 6bb61e3b7bce0931da574d19d1d82c88 + 0 | cfcd208495d565ef66e7dff9f98764da + 1 | c4ca4238a0b923820dcc509a6f75849b + 2 | c81e728d9d4c2f636f067f89cc14862c + 3 | eccbc87e4b5ce2fe28308fd9f2a7baf3 + 5 | e4da3b7fbbce2345d7772b0674a318d5 + 7 | 8f14e45fceea167a5a36dedd4bea2543 + 8 | c9f0f895fb98ab9159f51fd0297e236d + 9 | 45c48cce2e2d7fbdea1afc51c7c6ad26 + 10 | d3d9446802a44259755d38e6d163e820 + 12 | xxx + 4 | yyy +(21 rows) + -- -- ROLE/GROUP -- diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql index ed7adbf8d1..4da5035f5b 100644 --- a/src/test/regress/sql/rowsecurity.sql +++ b/src/test/regress/sql/rowsecurity.sql @@ -207,6 +207,8 @@ COPY t1 FROM stdin WITH (oids); \. CREATE TABLE t2 (c float) INHERITS (t1); +GRANT ALL ON t2 TO public; + COPY t2 FROM stdin WITH (oids); 201 1 abc 1.1 202 2 bcd 2.2 @@ -216,6 +218,8 @@ COPY t2 FROM stdin WITH (oids); CREATE TABLE t3 (c text, b text, a int) WITH OIDS; ALTER TABLE t3 INHERIT t1; +GRANT ALL ON t3 TO public; + COPY t3(a,b,c) FROM stdin WITH (oids); 301 1 xxx X 302 2 yyy Y @@ -423,9 +427,45 @@ UPDATE only t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1; UPDATE t1 SET b = b WHERE f_leak(b) RETURNING *; UPDATE t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1; +-- updates with from clause +EXPLAIN (COSTS OFF) UPDATE t2 SET b=t2.b FROM t3 +WHERE t2.a = 3 and t3.a = 2 AND f_leak(t2.b) AND f_leak(t3.b); + +UPDATE t2 SET b=t2.b FROM t3 +WHERE t2.a = 3 and t3.a = 2 AND f_leak(t2.b) AND f_leak(t3.b); + +EXPLAIN (COSTS OFF) UPDATE t1 SET b=t1.b FROM t2 +WHERE t1.a = 3 and t2.a = 3 AND f_leak(t1.b) AND f_leak(t2.b); + +UPDATE t1 SET b=t1.b FROM t2 +WHERE t1.a = 3 and t2.a = 3 AND f_leak(t1.b) AND f_leak(t2.b); + +EXPLAIN (COSTS OFF) UPDATE t2 SET b=t2.b FROM t1 +WHERE t1.a = 3 and t2.a = 3 AND f_leak(t1.b) AND f_leak(t2.b); + +UPDATE t2 SET b=t2.b FROM t1 +WHERE t1.a = 3 and t2.a = 3 AND f_leak(t1.b) AND f_leak(t2.b); + +-- updates with from clause self join +EXPLAIN (COSTS OFF) UPDATE t2 t2_1 SET b = t2_2.b FROM t2 t2_2 +WHERE t2_1.a = 3 AND t2_2.a = t2_1.a AND t2_2.b = t2_1.b +AND f_leak(t2_1.b) AND f_leak(t2_2.b) RETURNING *, t2_1, t2_2; + +UPDATE t2 t2_1 SET b = t2_2.b FROM t2 t2_2 +WHERE t2_1.a = 3 AND t2_2.a = t2_1.a AND t2_2.b = t2_1.b +AND f_leak(t2_1.b) AND f_leak(t2_2.b) RETURNING *, t2_1, t2_2; + +EXPLAIN (COSTS OFF) UPDATE t1 t1_1 SET b = t1_2.b FROM t1 t1_2 +WHERE t1_1.a = 4 AND t1_2.a = t1_1.a AND t1_2.b = t1_1.b +AND f_leak(t1_1.b) AND f_leak(t1_2.b) RETURNING *, t1_1, t1_2; + +UPDATE t1 t1_1 SET b = t1_2.b FROM t1 t1_2 +WHERE t1_1.a = 4 AND t1_2.a = t1_1.a AND t1_2.b = t1_1.b +AND f_leak(t1_1.b) AND f_leak(t1_2.b) RETURNING *, t1_1, t1_2; + RESET SESSION AUTHORIZATION; SET row_security TO OFF; -SELECT * FROM t1; +SELECT * FROM t1 ORDER BY a,b; SET SESSION AUTHORIZATION rls_regress_user1; SET row_security TO ON; @@ -435,6 +475,39 @@ EXPLAIN (COSTS OFF) DELETE FROM t1 WHERE f_leak(b); DELETE FROM only t1 WHERE f_leak(b) RETURNING oid, *, t1; DELETE FROM t1 WHERE f_leak(b) RETURNING oid, *, t1; +-- +-- S.b. view on top of Row-level security +-- +SET SESSION AUTHORIZATION rls_regress_user0; +CREATE TABLE b1 (a int, b text); +INSERT INTO b1 (SELECT x, md5(x::text) FROM generate_series(-10,10) x); + +CREATE POLICY p1 ON b1 USING (a % 2 = 0); +ALTER TABLE b1 ENABLE ROW LEVEL SECURITY; +GRANT ALL ON b1 TO rls_regress_user1; + +SET SESSION AUTHORIZATION rls_regress_user1; +CREATE VIEW bv1 WITH (security_barrier) AS SELECT * FROM b1 WHERE a > 0 WITH CHECK OPTION; +GRANT ALL ON bv1 TO rls_regress_user2; + +SET SESSION AUTHORIZATION rls_regress_user2; + +EXPLAIN (COSTS OFF) SELECT * FROM bv1 WHERE f_leak(b); +SELECT * FROM bv1 WHERE f_leak(b); + +INSERT INTO bv1 VALUES (-1, 'xxx'); -- should fail view WCO +INSERT INTO bv1 VALUES (11, 'xxx'); -- should fail RLS check +INSERT INTO bv1 VALUES (12, 'xxx'); -- ok + +EXPLAIN (COSTS OFF) UPDATE bv1 SET b = 'yyy' WHERE a = 4 AND f_leak(b); +UPDATE bv1 SET b = 'yyy' WHERE a = 4 AND f_leak(b); + +EXPLAIN (COSTS OFF) DELETE FROM bv1 WHERE a = 6 AND f_leak(b); +DELETE FROM bv1 WHERE a = 6 AND f_leak(b); + +SET SESSION AUTHORIZATION rls_regress_user0; +SELECT * FROM b1; + -- -- ROLE/GROUP --