aboutsummaryrefslogtreecommitdiff
path: root/src/backend/rewrite/rowsecurity.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/backend/rewrite/rowsecurity.c')
-rw-r--r--src/backend/rewrite/rowsecurity.c557
1 files changed, 557 insertions, 0 deletions
diff --git a/src/backend/rewrite/rowsecurity.c b/src/backend/rewrite/rowsecurity.c
new file mode 100644
index 00000000000..e1ccd1295e6
--- /dev/null
+++ b/src/backend/rewrite/rowsecurity.c
@@ -0,0 +1,557 @@
+/*
+ * rewrite/rowsecurity.c
+ * Routines to support policies for row-level security.
+ *
+ * Policies in PostgreSQL provide a mechanism to limit what records are
+ * returned to a user and what records a user is permitted to add to a table.
+ *
+ * Policies can be defined for specific roles, specific commands, or provided
+ * by an extension. Row security can also be enabled for a table without any
+ * policies being explicitly defined, in which case a default-deny policy is
+ * applied.
+ *
+ * Any part of the system which is returning records back to the user, or
+ * which is accepting records from the user to add to a table, needs to
+ * consider the policies associated with the table (if any). For normal
+ * queries, this is handled by calling prepend_row_security_policies() during
+ * rewrite, which looks at each RTE and adds the expressions defined by the
+ * policies to the securityQuals list for the RTE. For queries which modify
+ * the relation, any WITH CHECK policies are added to the list of
+ * WithCheckOptions for the Query and checked against each row which is being
+ * added to the table. Other parts of the system (eg: COPY) simply construct
+ * a normal query and use that, if RLS is to be applied.
+ *
+ * The check to see if RLS should be enabled is provided through
+ * check_enable_rls(), which returns an enum (defined in rowsecurity.h) to
+ * indicate if RLS should be enabled (RLS_ENABLED), or bypassed (RLS_NONE or
+ * RLS_NONE_ENV). RLS_NONE_ENV indicates that RLS should be bypassed
+ * in the current environment, but that may change if the row_security GUC or
+ * the current role changes.
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ */
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "access/htup_details.h"
+#include "access/sysattr.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_inherits_fn.h"
+#include "catalog/pg_rowsecurity.h"
+#include "catalog/pg_type.h"
+#include "miscadmin.h"
+#include "nodes/makefuncs.h"
+#include "nodes/nodeFuncs.h"
+#include "nodes/pg_list.h"
+#include "nodes/plannodes.h"
+#include "parser/parsetree.h"
+#include "rewrite/rewriteHandler.h"
+#include "rewrite/rewriteManip.h"
+#include "rewrite/rowsecurity.h"
+#include "utils/acl.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "tcop/utility.h"
+
+static List *pull_row_security_policies(CmdType cmd, Relation relation,
+ Oid user_id);
+static void process_policies(List *policies, int rt_index,
+ Expr **final_qual,
+ Expr **final_with_check_qual,
+ bool *hassublinks);
+static bool check_role_for_policy(RowSecurityPolicy *policy, Oid user_id);
+
+/*
+ * hook to allow extensions to apply their own security policy
+ *
+ * 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;
+
+/*
+ * 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.
+ *
+ * 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.
+ */
+bool
+prepend_row_security_policies(Query* root, RangeTblEntry* rte, int rt_index)
+{
+ Expr *rowsec_expr = NULL;
+ Expr *rowsec_with_check_expr = NULL;
+ Expr *hook_expr = NULL;
+ Expr *hook_with_check_expr = NULL;
+
+ List *rowsec_policies;
+ List *hook_policies = NIL;
+
+ Relation rel;
+ Oid user_id;
+ int sec_context;
+ int rls_status;
+ bool defaultDeny = true;
+ bool hassublinks = false;
+
+ /* This is just to get the security context */
+ GetUserIdAndSecContext(&user_id, &sec_context);
+
+ /* Switch to checkAsUser if it's set */
+ user_id = rte->checkAsUser ? rte->checkAsUser : GetUserId();
+
+ /*
+ * If this is not a normal relation, or we have been told
+ * to explicitly skip RLS (perhaps because this is an FK check)
+ * then just return immediately.
+ */
+ if (rte->relid < FirstNormalObjectId
+ || rte->relkind != RELKIND_RELATION
+ || (sec_context & SECURITY_ROW_LEVEL_DISABLED))
+ return false;
+
+ /* Determine the state of RLS for this, pass checkAsUser explicitly */
+ rls_status = check_enable_rls(rte->relid, rte->checkAsUser);
+
+ /* If there is no RLS on this table at all, nothing to do */
+ if (rls_status == RLS_NONE)
+ return false;
+
+ /*
+ * RLS_NONE_ENV means we are not doing any RLS now, but that may change
+ * with changes to the environment, so we mark it as hasRowSecurity to
+ * force a re-plan when the environment changes.
+ */
+ if (rls_status == RLS_NONE_ENV)
+ {
+ /*
+ * Indicate that this query may involve RLS and must therefore
+ * be replanned if the environment changes (GUCs, role), but we
+ * are not adding anything here.
+ */
+ root->hasRowSecurity = true;
+
+ return false;
+ }
+
+ /*
+ * 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);
+
+ rowsec_policies = pull_row_security_policies(root->commandType, rel,
+ user_id);
+
+ /*
+ * Check if this is only the default-deny policy.
+ *
+ * Normally, if the table has row-security enabled but there are
+ * no policies, we use a default-deny policy and not allow anything.
+ * However, when an extension uses the hook to add their own
+ * policies, we don't want to include the default deny policy or
+ * there won't be any way for a user to use an extension exclusively
+ * for the policies to be used.
+ */
+ if (((RowSecurityPolicy *) linitial(rowsec_policies))->rsecid
+ == InvalidOid)
+ 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);
+
+ /*
+ * Also, allow extensions to add their own 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).
+ *
+ * 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
+ * handled.
+ *
+ * The only caveat to this is that if there are NO internal policies
+ * defined, there ARE policies returned by the extension, and RLS is
+ * enabled on the table, then we will ignore the internally-generated
+ * default-deny policy and use only the policies returned by the
+ * extension.
+ */
+ if (row_security_policy_hook)
+ {
+ hook_policies = (*row_security_policy_hook)(root->commandType, rel);
+
+ /* Build the expression from any policies returned. */
+ process_policies(hook_policies, rt_index, &hook_expr,
+ &hook_with_check_expr, &hassublinks);
+ }
+
+ /*
+ * 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).
+ */
+ if (defaultDeny && hook_policies != NIL)
+ rowsec_expr = NULL;
+
+ /*
+ * For INSERT or UPDATE, we need to add the WITH CHECK quals to
+ * Query's withCheckOptions to verify that any new records pass the
+ * WITH CHECK policy (this will be a copy of the USING policy, if no
+ * explicit WITH CHECK policy exists).
+ */
+ if (root->commandType == CMD_INSERT || root->commandType == CMD_UPDATE)
+ {
+ /*
+ * WITH CHECK OPTIONS wants a WCO node which wraps each Expr, so
+ * create them as necessary.
+ */
+ if (rowsec_with_check_expr)
+ {
+ WithCheckOption *wco;
+
+ wco = (WithCheckOption *) makeNode(WithCheckOption);
+ wco->viewname = RelationGetRelationName(rel);
+ wco->qual = (Node *) rowsec_with_check_expr;
+ wco->cascaded = false;
+ root->withCheckOptions = lcons(wco, root->withCheckOptions);
+ }
+
+ /*
+ * Ditto for the expression, if any, returned from the extension.
+ */
+ if (hook_with_check_expr)
+ {
+ WithCheckOption *wco;
+
+ wco = (WithCheckOption *) makeNode(WithCheckOption);
+ wco->viewname = RelationGetRelationName(rel);
+ wco->qual = (Node *) hook_with_check_expr;
+ wco->cascaded = false;
+ root->withCheckOptions = lcons(wco, root->withCheckOptions);
+ }
+ }
+
+ /* For SELECT, UPDATE, and DELETE, set the security quals */
+ if (root->commandType == CMD_SELECT
+ || root->commandType == CMD_UPDATE
+ || root->commandType == CMD_DELETE)
+ {
+ if (rowsec_expr)
+ rte->securityQuals = lcons(rowsec_expr, rte->securityQuals);
+
+ if (hook_expr)
+ rte->securityQuals = lcons(hook_expr,
+ rte->securityQuals);
+ }
+
+ heap_close(rel, NoLock);
+
+ /*
+ * Mark this query as having row security, so plancache can invalidate
+ * it when necessary (eg: role changes)
+ */
+ root->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.
+ */
+ if (hassublinks)
+ root->hasSubLinks = hassublinks;
+
+ /* If we got this far, we must have added quals */
+ return true;
+}
+
+/*
+ * pull_row_security_policies
+ *
+ * Returns the list of policies to be added for this relation, based on the
+ * type of command and the roles to which it applies, from the relation cache.
+ *
+ */
+static List *
+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
+ * either 'on' or 'force' here, so find the policies to apply to the table.
+ * There must always be at least one policy defined (may be the simple
+ * 'default-deny' policy, if none are explicitly defined on the table).
+ */
+ foreach(item, relation->rsdesc->policies)
+ {
+ policy = (RowSecurityPolicy *) lfirst(item);
+
+ /* Always add ALL policies, if they exist. */
+ if (policy->cmd == '\0' && check_role_for_policy(policy, user_id))
+ policies = lcons(policy, policies);
+
+ /* Build the list of policies to return. */
+ switch(cmd)
+ {
+ case CMD_SELECT:
+ if (policy->cmd == ACL_SELECT_CHR
+ && check_role_for_policy(policy, user_id))
+ policies = lcons(policy, policies);
+ break;
+ case CMD_INSERT:
+ /* If INSERT then only need to add the WITH CHECK qual */
+ if (policy->cmd == ACL_INSERT_CHR
+ && check_role_for_policy(policy, user_id))
+ policies = lcons(policy, policies);
+ break;
+ case CMD_UPDATE:
+ if (policy->cmd == ACL_UPDATE_CHR
+ && check_role_for_policy(policy, user_id))
+ policies = lcons(policy, policies);
+ break;
+ case CMD_DELETE:
+ if (policy->cmd == ACL_DELETE_CHR
+ && check_role_for_policy(policy, user_id))
+ policies = lcons(policy, policies);
+ break;
+ default:
+ elog(ERROR, "unrecognized command type.");
+ break;
+ }
+ }
+
+ /*
+ * There should always be a policy applied. If there are none found then
+ * create a simply defauly-deny policy (might be that policies exist but
+ * that none of them apply to the role which is querying the table).
+ */
+ if (policies == NIL)
+ {
+ RowSecurityPolicy *policy = NULL;
+ Datum role;
+
+ role = ObjectIdGetDatum(ACL_ID_PUBLIC);
+
+ policy = palloc0(sizeof(RowSecurityPolicy));
+ policy->policy_name = pstrdup("default-deny policy");
+ policy->rsecid = InvalidOid;
+ policy->cmd = '\0';
+ policy->roles = construct_array(&role, 1, OIDOID, sizeof(Oid), true,
+ 'i');
+ policy->qual = (Expr *) makeConst(BOOLOID, -1, InvalidOid,
+ sizeof(bool), BoolGetDatum(false),
+ false, true);
+ policy->with_check_qual = copyObject(policy->qual);
+ policy->hassublinks = false;
+
+ policies = list_make1(policy);
+ }
+
+ Assert(policies != NIL);
+
+ return policies;
+}
+
+/*
+ * process_policies
+ *
+ * This will step through the policies which are passed in (which would come
+ * from either the built-in ones created on a table, or from policies provided
+ * by an extension through the hook provided), work out how to combine them,
+ * rewrite them as necessary, and produce an Expr for the normal security
+ * quals and an Expr for the with check quals.
+ *
+ * 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)
+{
+ ListCell *item;
+ List *quals = NIL;
+ List *with_check_quals = NIL;
+
+ /*
+ * Extract the USING and WITH CHECK quals from each of the policies
+ * and add them to our lists.
+ */
+ foreach(item, policies)
+ {
+ RowSecurityPolicy *policy = (RowSecurityPolicy *) lfirst(item);
+
+ if (policy->qual != NULL)
+ quals = lcons(copyObject(policy->qual), quals);
+
+ if (policy->with_check_qual != NULL)
+ with_check_quals = lcons(copyObject(policy->with_check_qual),
+ with_check_quals);
+
+ if (policy->hassublinks)
+ *hassublinks = true;
+ }
+
+ /*
+ * If we end up without any normal quals (perhaps the only policy matched
+ * was for INSERT), then create a single all-false one.
+ */
+ if (quals == NIL)
+ 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
+ * expression, updating any references to varno 1 to the varno
+ * the table has in the outer query.
+ *
+ * We rewrite the expression in-place.
+ */
+ ChangeVarNodes((Node *) quals, 1, rt_index, 0);
+ 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.
+ */
+ if (list_length(quals) > 1)
+ *qual_eval = makeBoolExpr(OR_EXPR, 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.
+ */
+ if (list_length(with_check_quals) > 1)
+ *with_check_eval = makeBoolExpr(OR_EXPR, with_check_quals, -1);
+ else
+ *with_check_eval = (Expr*) linitial(with_check_quals);
+
+ return;
+}
+
+/*
+ * check_enable_rls
+ *
+ * Determine, based on the relation, row_security setting, and current role,
+ * if RLS is applicable to this query. RLS_NONE_ENV indicates that, while
+ * RLS is not to be added for this query, a change in the environment may change
+ * that. RLS_NONE means that RLS is not on the relation at all and therefore
+ * we don't need to worry about it. RLS_ENABLED means RLS should be implemented
+ * for the table and the plan cache needs to be invalidated if the environment
+ * changes.
+ *
+ * Handle checking as another role via checkAsUser (for views, etc).
+ */
+int
+check_enable_rls(Oid relid, Oid checkAsUser)
+{
+ HeapTuple tuple;
+ Form_pg_class classform;
+ bool relhasrowsecurity;
+ Oid user_id = checkAsUser ? checkAsUser : GetUserId();
+
+ tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+ if (!HeapTupleIsValid(tuple))
+ return RLS_NONE;
+
+ classform = (Form_pg_class) GETSTRUCT(tuple);
+
+ relhasrowsecurity = classform->relhasrowsecurity;
+
+ ReleaseSysCache(tuple);
+
+ /* Nothing to do if the relation does not have RLS */
+ if (!relhasrowsecurity)
+ return RLS_NONE;
+
+ /*
+ * Check permissions
+ *
+ * If the relation has row level security enabled and the row_security GUC
+ * is off, then check if the user has rights to bypass RLS for this
+ * relation. Table owners can always bypass, as can any role with the
+ * BYPASSRLS capability.
+ *
+ * If the role is the table owner, then we bypass RLS unless row_security
+ * is set to 'force'. Note that superuser is always considered an owner.
+ *
+ * Return RLS_NONE_ENV to indicate that this decision depends on the
+ * environment (in this case, what the current values of user_id and
+ * row_security are).
+ */
+ if (row_security != ROW_SECURITY_FORCE
+ && (pg_class_ownercheck(relid, user_id)))
+ return RLS_NONE_ENV;
+
+ /*
+ * If the row_security GUC is 'off' then check if the user has permission
+ * to bypass it. Note that we have already handled the case where the user
+ * is the table owner above.
+ *
+ * Note that row_security is always considered 'on' when querying
+ * through a view or other cases where checkAsUser is true, so skip this
+ * if checkAsUser is in use.
+ */
+ if (!checkAsUser && row_security == ROW_SECURITY_OFF)
+ {
+ if (has_bypassrls_privilege(user_id))
+ /* OK to bypass */
+ return RLS_NONE_ENV;
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("insufficient privilege to bypass row security.")));
+ }
+
+ /* RLS should be fully enabled for this relation. */
+ return RLS_ENABLED;
+}
+
+/*
+ * check_role_for_policy -
+ * determines if the policy should be applied for the current role
+ */
+bool
+check_role_for_policy(RowSecurityPolicy *policy, Oid user_id)
+{
+ int i;
+ Oid *roles = (Oid *) ARR_DATA_PTR(policy->roles);
+
+ /* Quick fall-thru for policies applied to all roles */
+ if (roles[0] == ACL_ID_PUBLIC)
+ return true;
+
+ for (i = 0; i < ARR_DIMS(policy->roles)[0]; i++)
+ {
+ if (is_member_of_role(user_id, roles[i]))
+ return true;
+ }
+
+ return false;
+}