+ pg_rowlevelsec Columns
+
+
+
+ Name
+ Type
+ References
+ Description
+
+
+
+
+ rlsrelid
+ oid
+ pg_class.oid
+ The table this row-level security is for
+
+
+ rlsqual
+ text
+
+ An expression tree to be performed as rowl-level security policy
+
+
+
+
+
+
+ pg_class.relhasrowlevelsec
+ must be true if a table has row-level security policy in this catalog.
+
+
+ pg_seclabel
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 356419e..4f2cd37 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -68,6 +68,8 @@ ALTER TABLE [ IF EXISTS ] name
NOT OF
OWNER TO new_owner
SET TABLESPACE new_tablespace
+ SET ROW LEVEL SECURITY (condition)
+ RESET ROW LEVEL SECURITY
and table_constraint_using_index is:
@@ -567,6 +569,29 @@ ALTER TABLE [ IF EXISTS ] name
+ SET ROW LEVEL SECURITY (condition)
+
+
+ This form set row-level security policy of the table.
+ Supplied condition performs
+ as if it is implicitly appended to the qualifiers of WHERE
+ clause, although mechanism guarantees to evaluate this condition earlier
+ than any other user given condition.
+ See also .
+
+
+
+
+
+ RESET ROW LEVEL SECURITY
+
+
+ This form reset row-level security policy of the table, if exists.
+
+
+
+
+ RENAME
@@ -808,6 +833,20 @@ ALTER TABLE [ IF EXISTS ] name
+
+ condition
+
+
+ An expression that returns a value of type boolean. Expect for a case
+ when queries are executed with superuser privilege, only rows for which
+ this expression returns true will be fetched, updated or deleted.
+ This expression can reference columns of the relation being configured.
+ Sub-queries can be contained within expression tree, unless referenced
+ relation recursively references the same relation.
+
+
+
+
diff --git a/doc/src/sgml/user-manag.sgml b/doc/src/sgml/user-manag.sgml
index 177ac7a..dab5b1e 100644
--- a/doc/src/sgml/user-manag.sgml
+++ b/doc/src/sgml/user-manag.sgml
@@ -439,4 +439,172 @@ DROP ROLE name;
+
+ Row-level Security
+
+ PostgreSQL v9.3 or later provides
+ row-level security feature, like several commercial database
+ management system. It allows table owner to assign a particular
+ condition that performs as a security policy of the table; only
+ rows that satisfies the condition should be visible, except for
+ a case when superuser runs queries.
+
+
+ Row-level security policy can be set using
+ SET ROW LEVEL SECURITY command of
+ statement, as an expression
+ form that returns a value of type boolean. This expression can
+ contain references to columns of the relation, so it enables
+ to construct arbitrary rule to make access control decision
+ based on contents of each rows.
+
+
+ For example, the following customer table
+ has uname field to store user name, and
+ it assume we don't want to expose any properties of other
+ customers.
+ The following command set current_user = uname
+ as row-level security policy on the customer
+ table.
+
+postgres=> ALTER TABLE customer SET ROW LEVEL SECURITY (current_user = uname);
+ALTER TABLE
+
+ command shows how row-level
+ security policy works on the supplied query.
+
+postgres=> EXPLAIN(costs off) SELECT * FROM customer WHERE f_leak(upasswd);
+ QUERY PLAN
+--------------------------------------------
+ Subquery Scan on customer
+ Filter: f_leak(customer.upasswd)
+ -> Seq Scan on customer customer_1
+ Filter: ("current_user"() = uname)
+(4 rows)
+
+ This query execution plan means the preconfigured row-level
+ security policy is implicitly added, and scan plan on the
+ target relation being wrapped up with a sub-query.
+ It ensures user given qualifiers, including functions with
+ side effects, are never executed earlier than the row-level
+ security policy regardless of its cost, except for the cases
+ when these were fully leakproof.
+ This design helps to tackle the scenario described in
+ ; that introduces the order
+ to evaluate qualifiers is significant to keep confidentiality
+ of invisible rows.
+
+
+
+ On the other hand, this design allows superusers to bypass
+ checks with row-level security.
+ It ensures pg_dump can obtain
+ a complete set of database backup, and avoid to execute
+ Trojan horse trap, being injected as a row-level security
+ policy of user-defined table, with privileges of superuser.
+
+
+
+ In case of queries on inherited tables, row-level security
+ policy of the parent relation is not applied to child
+ relations. Scope of the row-level security policy is limited
+ to the relation on which it is set.
+
+postgres=> EXPLAIN(costs off) SELECT * FROM t1 WHERE f_leak(y);
+ QUERY PLAN
+-------------------------------------------
+ Result
+ -> Append
+ -> Subquery Scan on t1
+ Filter: f_leak(t1.y)
+ -> Seq Scan on t1 t1_1
+ Filter: ((x % 2) = 0)
+ -> Seq Scan on t2
+ Filter: f_leak(y)
+ -> Subquery Scan on t3
+ Filter: f_leak(t3.y)
+ -> Seq Scan on t3 t3_1
+ Filter: ((x % 2) = 1)
+(12 rows)
+
+ In the above example, t1 has inherited
+ child table t2 and t3,
+ and row-level security policy is set on only t1,
+ and t3, not t2.
+
+ The row-level security policy of t1,
+ x must be even-number, is appended only
+ t1, neither t2 nor
+ t3. On the contrary, t3
+ has different row-level security policy; x
+ must be odd-number.
+
+
+
+ Row-level security feature also works to queries for writer-
+ operations; such as or
+ commands.
+ It prevents to modify rows that does not satisfy the configured
+ row-level security policy.
+ The below query tries to update e-mail address of the
+ customer> table, and the row-level security makes
+ sure any rows that don't match with current_user>.
+
+
+postgres=> EXPLAIN (costs off) UPDATE customer SET email = 'alice@example.com';
+ QUERY PLAN
+--------------------------------------------------
+ Update on customer
+ -> Subquery Scan on customer
+ -> Seq Scan on customer customer_1
+ Filter: ("current_user"() = uname)
+(4 rows)
+
+
+ On the other hands, please note that row-level security is
+ not applied on command, because
+ of no sense.
+ Any row-level security shall be applied on the timing when
+ rows are fetched from the tables, prior to evaluation of
+ user given WHERE> clause to prevent unexpected
+ information leaks using non-leakproof functions.
+ All the rows to be inserted are. at least, visible for
+ current session users. therefore, it makes no sense to
+ check something from viewpoint of row-level security.
+
+
+ If you want to apply some constraints on rows to be inserted
+ or updated, we recommend to set up CHECK>
+ constraints or before row triggers, rather than row-level
+ security features here.
+ Please also check the
+ section to understand leaky-view scenario bahalf on
+ the row-level security feature.
+
+
+
+ Unlike other commercial database systems, we don't have any
+ plan to allow individual row-level security policy for each
+ command type. Even if we want to perform with difference
+ policy between and
+ , RETURNING> clause
+ can leak the rows to be invisible using
+ command.
+
+
+
+ Even though it is not a specific matter in row-level security,
+ please be careful if you plan to use current_user>
+ in row-level security policy.
+ and
+ allows to switch current user identifier during execution
+ of the query. Thus, it can fetch the first 100 rows with
+ privilege of alice>, then remaining rows with
+ privilege of bob>. If and when query execution plan
+ contains some kind of materialization and row-level security
+ policy contains current_user>, the fetched tuples
+ in bob>'s screen might be evaluated according to
+ the privilege of alice>.
+
+
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index df6da1f..965aa38 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -14,7 +14,7 @@ OBJS = catalog.o dependency.o heap.o index.o indexing.o namespace.o aclchk.o \
objectaddress.o pg_aggregate.o pg_collation.o pg_constraint.o pg_conversion.o \
pg_depend.o pg_enum.o pg_inherits.o pg_largeobject.o pg_namespace.o \
pg_operator.o pg_proc.o pg_range.o pg_db_role_setting.o pg_shdepend.o \
- pg_type.o storage.o toasting.o
+ pg_rowlevelsec.o pg_type.o storage.o toasting.o
BKIFILES = postgres.bki postgres.description postgres.shdescription
@@ -38,7 +38,7 @@ POSTGRES_BKI_SRCS = $(addprefix $(top_srcdir)/src/include/catalog/,\
pg_ts_config.h pg_ts_config_map.h pg_ts_dict.h \
pg_ts_parser.h pg_ts_template.h pg_extension.h \
pg_foreign_data_wrapper.h pg_foreign_server.h pg_user_mapping.h \
- pg_foreign_table.h \
+ pg_foreign_table.h pg_rowlevelsec.h \
pg_default_acl.h pg_seclabel.h pg_shseclabel.h pg_collation.h pg_range.h \
toasting.h indexing.h \
)
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index b9cfee2..2142c36 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -47,6 +47,7 @@
#include "catalog/pg_opfamily.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_rewrite.h"
+#include "catalog/pg_rowlevelsec.h"
#include "catalog/pg_tablespace.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_ts_config.h"
@@ -1240,6 +1241,10 @@ doDeletion(const ObjectAddress *object, int flags)
RemoveExtensionById(object->objectId);
break;
+ case OCLASS_ROWLEVELSEC:
+ RemoveRowLevelSecurityById(object->objectId);
+ break;
+
default:
elog(ERROR, "unrecognized object class: %u",
object->classId);
@@ -2291,6 +2296,9 @@ getObjectClass(const ObjectAddress *object)
case EventTriggerRelationId:
return OCLASS_EVENT_TRIGGER;
+
+ case RowLevelSecurityRelationId:
+ return OCLASS_ROWLEVELSEC;
}
/* shouldn't get here */
@@ -2940,6 +2948,21 @@ getObjectDescription(const ObjectAddress *object)
break;
}
+ case OCLASS_ROWLEVELSEC:
+ {
+ char *relname;
+
+ relname = get_rel_name(object->objectId);
+ if (!relname)
+ elog(ERROR, "cache lookup failed for relation %u",
+ object->objectId);
+ appendStringInfo(&buffer,
+ _("row-level security of %s"), relname);
+
+
+ break;
+ }
+
default:
appendStringInfo(&buffer, "unrecognized object %u %u %d",
object->classId,
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 8818b68..cc56143 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -776,6 +776,7 @@ InsertPgClassTuple(Relation pg_class_desc,
values[Anum_pg_class_relhaspkey - 1] = BoolGetDatum(rd_rel->relhaspkey);
values[Anum_pg_class_relhasrules - 1] = BoolGetDatum(rd_rel->relhasrules);
values[Anum_pg_class_relhastriggers - 1] = BoolGetDatum(rd_rel->relhastriggers);
+ values[Anum_pg_class_relhasrowlevelsec - 1] = BoolGetDatum(rd_rel->relhasrowlevelsec);
values[Anum_pg_class_relhassubclass - 1] = BoolGetDatum(rd_rel->relhassubclass);
values[Anum_pg_class_relfrozenxid - 1] = TransactionIdGetDatum(rd_rel->relfrozenxid);
if (relacl != (Datum) 0)
diff --git a/src/backend/catalog/pg_rowlevelsec.c b/src/backend/catalog/pg_rowlevelsec.c
new file mode 100644
index 0000000..f0e49a6
--- /dev/null
+++ b/src/backend/catalog/pg_rowlevelsec.c
@@ -0,0 +1,288 @@
+/* -------------------------------------------------------------------------
+ *
+ * pg_rowlevelsec.c
+ * routines to support manipulation of the pg_rowlevelsec relation
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * -------------------------------------------------------------------------
+ */
+#include "postgres.h"
+#include "access/genam.h"
+#include "access/heapam.h"
+#include "access/htup_details.h"
+#include "catalog/dependency.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_rowlevelsec.h"
+#include "catalog/pg_type.h"
+#include "nodes/nodeFuncs.h"
+#include "optimizer/clauses.h"
+#include "parser/parse_clause.h"
+#include "parser/parse_node.h"
+#include "parser/parse_relation.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/tqual.h"
+
+/*
+ * Load row-level security policy from the catalog, and keep it on
+ * the relation cache.
+ */
+void
+RelationBuildRowLevelSecurity(Relation relation)
+{
+ Relation rlsrel;
+ ScanKeyData skey;
+ SysScanDesc sscan;
+ HeapTuple tuple;
+
+ rlsrel = heap_open(RowLevelSecurityRelationId, AccessShareLock);
+
+ ScanKeyInit(&skey,
+ Anum_pg_rowlevelsec_rlsrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(RelationGetRelid(relation)));
+ sscan = systable_beginscan(rlsrel, RowLevelSecurityIndexId, true,
+ SnapshotNow, 1, &skey);
+
+ tuple = systable_getnext(sscan);
+ if (HeapTupleIsValid(tuple))
+ {
+ RowLevelSecDesc *rlsdesc;
+ MemoryContext rlscxt;
+ MemoryContext oldcxt;
+ Datum datum;
+ bool isnull;
+ char *temp;
+
+ /*
+ * Make the private memory context to store RowLevelSecDesc that
+ * includes expression tree also.
+ */
+ rlscxt = AllocSetContextCreate(CacheMemoryContext,
+ RelationGetRelationName(relation),
+ ALLOCSET_SMALL_MINSIZE,
+ ALLOCSET_SMALL_INITSIZE,
+ ALLOCSET_SMALL_MAXSIZE);
+ PG_TRY();
+ {
+ datum = heap_getattr(tuple, Anum_pg_rowlevelsec_rlsqual,
+ RelationGetDescr(rlsrel), &isnull);
+ Assert(!isnull);
+ temp = TextDatumGetCString(datum);
+
+ oldcxt = MemoryContextSwitchTo(rlscxt);
+
+ rlsdesc = palloc0(sizeof(RowLevelSecDesc));
+ rlsdesc->rlscxt = rlscxt;
+ rlsdesc->rlsqual = (Expr *) stringToNode(temp);
+ Assert(exprType((Node *)rlsdesc->rlsqual) == BOOLOID);
+
+ rlsdesc->rlshassublinks
+ = contain_subplans((Node *)rlsdesc->rlsqual);
+
+ MemoryContextSwitchTo(oldcxt);
+
+ pfree(temp);
+ }
+ PG_CATCH();
+ {
+ MemoryContextDelete(rlscxt);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ relation->rlsdesc = rlsdesc;
+ }
+ else
+ {
+ relation->rlsdesc = NULL;
+ }
+ systable_endscan(sscan);
+ heap_close(rlsrel, AccessShareLock);
+}
+
+/*
+ * Parse the supplied row-level security policy, and insert/update a row
+ * of pg_rowlevelsec catalog.
+ */
+static void
+InsertOrUpdatePolicyRow(Relation relation, Node *clause)
+{
+ Oid relationId = RelationGetRelid(relation);
+ ParseState *pstate;
+ RangeTblEntry *rte;
+ Node *qual;
+ Relation rlsrel;
+ ScanKeyData skey;
+ SysScanDesc sscan;
+ HeapTuple oldtup;
+ HeapTuple newtup;
+ Datum values[Natts_pg_rowlevelsec];
+ bool isnull[Natts_pg_rowlevelsec];
+ bool replaces[Natts_pg_rowlevelsec];
+ ObjectAddress target;
+ ObjectAddress myself;
+
+ /* Parse the supplied clause */
+ pstate = make_parsestate(NULL);
+
+ rte = addRangeTableEntryForRelation(pstate, relation,
+ NULL, false, false);
+ addRTEtoQuery(pstate, rte, false, true, true);
+
+ qual = transformWhereClause(pstate, copyObject(clause),
+ EXPR_KIND_ROW_LEVEL_SEC,
+ "ROW LEVEL SECURITY");
+ /* zero-clear */
+ memset(values, 0, sizeof(values));
+ memset(replaces, 0, sizeof(replaces));
+ memset(isnull, 0, sizeof(isnull));
+
+ /* Update or Insert an entry to pg_rowlevelsec catalog */
+ rlsrel = heap_open(RowLevelSecurityRelationId, RowExclusiveLock);
+
+ ScanKeyInit(&skey,
+ Anum_pg_rowlevelsec_rlsrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(RelationGetRelid(relation)));
+ sscan = systable_beginscan(rlsrel, RowLevelSecurityIndexId, true,
+ SnapshotNow, 1, &skey);
+ oldtup = systable_getnext(sscan);
+ if (HeapTupleIsValid(oldtup))
+ {
+ replaces[Anum_pg_rowlevelsec_rlsqual - 1] = true;
+ values[Anum_pg_rowlevelsec_rlsqual - 1]
+ = CStringGetTextDatum(nodeToString(qual));
+
+ newtup = heap_modify_tuple(oldtup,
+ RelationGetDescr(rlsrel),
+ values, isnull, replaces);
+ simple_heap_update(rlsrel, &newtup->t_self, newtup);
+
+ deleteDependencyRecordsFor(RowLevelSecurityRelationId,
+ relationId, false);
+ }
+ else
+ {
+ values[Anum_pg_rowlevelsec_rlsrelid - 1]
+ = ObjectIdGetDatum(relationId);
+ values[Anum_pg_rowlevelsec_rlsqual - 1]
+ = CStringGetTextDatum(nodeToString(qual));
+ newtup = heap_form_tuple(RelationGetDescr(rlsrel),
+ values, isnull);
+ simple_heap_insert(rlsrel, newtup);
+ }
+ CatalogUpdateIndexes(rlsrel, newtup);
+
+ heap_freetuple(newtup);
+
+ /* records dependencies of RLS-policy and relation/columns */
+ target.classId = RelationRelationId;
+ target.objectId = relationId;
+ target.objectSubId = 0;
+
+ myself.classId = RowLevelSecurityRelationId;
+ myself.objectId = relationId;
+ myself.objectSubId = 0;
+
+ recordDependencyOn(&myself, &target, DEPENDENCY_AUTO);
+
+ recordDependencyOnExpr(&myself, qual, pstate->p_rtable,
+ DEPENDENCY_NORMAL);
+ free_parsestate(pstate);
+
+ systable_endscan(sscan);
+ heap_close(rlsrel, RowExclusiveLock);
+}
+
+/*
+ * Remove row-level security policy row of pg_rowlevelsec
+ */
+static void
+DeletePolicyRow(Relation relation)
+{
+ if (relation->rlsdesc)
+ {
+ ObjectAddress address;
+
+ address.classId = RowLevelSecurityRelationId;
+ address.objectId = RelationGetRelid(relation);
+ address.objectSubId = 0;
+
+ performDeletion(&address, DROP_RESTRICT, 0);
+ }
+ else
+ {
+ /* Nothing to do here */
+ elog(INFO, "relation %s has no row-level security policy, skipped",
+ RelationGetRelationName(relation));
+ }
+}
+
+/*
+ * Guts of row-level security policy deletion.
+ */
+void
+RemoveRowLevelSecurityById(Oid relationId)
+{
+ Relation rlsrel;
+ ScanKeyData skey;
+ SysScanDesc sscan;
+ HeapTuple tuple;
+
+ rlsrel = heap_open(RowLevelSecurityRelationId, RowExclusiveLock);
+
+ ScanKeyInit(&skey,
+ Anum_pg_rowlevelsec_rlsrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(relationId));
+ sscan = systable_beginscan(rlsrel, RowLevelSecurityIndexId, true,
+ SnapshotNow, 1, &skey);
+ while (HeapTupleIsValid(tuple = systable_getnext(sscan)))
+ {
+ simple_heap_delete(rlsrel, &tuple->t_self);
+ }
+ systable_endscan(sscan);
+ heap_close(rlsrel, RowExclusiveLock);
+}
+
+/*
+ * ALTER TABLE SET ROW LEVEL SECURITY (...) OR
+ * RESET ROW LEVEL SECURITY
+ */
+void
+SetRowLevelSecurity(Relation relation, Node *clause)
+{
+ Oid relid = RelationGetRelid(relation);
+ Relation class_rel;
+ HeapTuple tuple;
+ Form_pg_class class_form;
+
+ class_rel = heap_open(RelationRelationId, RowExclusiveLock);
+
+ tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for relation %u", relid);
+
+ class_form = (Form_pg_class) GETSTRUCT(tuple);
+ if (clause != NULL)
+ {
+ InsertOrUpdatePolicyRow(relation, clause);
+ class_form->relhasrowlevelsec = true;
+ }
+ else
+ {
+ DeletePolicyRow(relation);
+ class_form->relhasrowlevelsec = false;
+ }
+ simple_heap_update(class_rel, &tuple->t_self, tuple);
+ CatalogUpdateIndexes(class_rel, tuple);
+
+ heap_close(class_rel, RowExclusiveLock);
+ heap_freetuple(tuple);
+}
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 10c89c7..7ffe4a3 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -24,6 +24,7 @@
#include "access/htup_details.h"
#include "access/sysattr.h"
#include "access/xact.h"
+#include "catalog/heap.h"
#include "catalog/namespace.h"
#include "catalog/pg_type.h"
#include "commands/copy.h"
@@ -34,15 +35,19 @@
#include "libpq/pqformat.h"
#include "mb/pg_wchar.h"
#include "miscadmin.h"
+#include "nodes/makefuncs.h"
#include "optimizer/clauses.h"
#include "optimizer/planner.h"
+#include "optimizer/rowlevelsec.h"
#include "parser/parse_relation.h"
+#include "parser/parsetree.h"
#include "rewrite/rewriteHandler.h"
#include "storage/fd.h"
#include "tcop/tcopprot.h"
#include "utils/acl.h"
#include "utils/builtins.h"
#include "utils/lsyscache.h"
+#include "utils/syscache.h"
#include "utils/memutils.h"
#include "utils/rel.h"
#include "utils/snapmgr.h"
@@ -742,7 +747,7 @@ CopyLoadRawBuf(CopyState cstate)
* the table or the specifically requested columns.
*/
uint64
-DoCopy(const CopyStmt *stmt, const char *queryString)
+DoCopy(CopyStmt *stmt, const char *queryString)
{
CopyState cstate;
bool is_from = stmt->is_from;
@@ -772,14 +777,26 @@ DoCopy(const CopyStmt *stmt, const char *queryString)
rel = heap_openrv(stmt->relation,
(is_from ? RowExclusiveLock : AccessShareLock));
+ tupDesc = RelationGetDescr(rel);
+ attnums = CopyGetAttnums(tupDesc, rel, stmt->attlist);
+
+ /*
+ * We have to run regular query, if the target relation has
+ * row-level security policy
+ */
+ if (copy_row_level_security(stmt, rel, attnums))
+ {
+ heap_close(rel, NoLock); /* close with keeping lock */
+ rel = NULL;
+ }
+ else
+ {
rte = makeNode(RangeTblEntry);
rte->rtekind = RTE_RELATION;
rte->relid = RelationGetRelid(rel);
rte->relkind = rel->rd_rel->relkind;
rte->requiredPerms = required_access;
- tupDesc = RelationGetDescr(rel);
- attnums = CopyGetAttnums(tupDesc, rel, stmt->attlist);
foreach(cur, attnums)
{
int attno = lfirst_int(cur) -
@@ -791,6 +808,7 @@ DoCopy(const CopyStmt *stmt, const char *queryString)
rte->selectedCols = bms_add_member(rte->selectedCols, attno);
}
ExecCheckRTPerms(list_make1(rte), true);
+ }
}
else
{
@@ -1139,6 +1157,53 @@ ProcessCopyOptions(CopyState cstate,
}
/*
+ * Adjust Query tree constructed with row-level security feature.
+ * If WITH OIDS option was supplied, it adds Var node to reference
+ * object-id system column.
+ */
+static void
+fixup_oid_of_rls_query(Query *query)
+{
+ RangeTblEntry *subrte;
+ TargetEntry *subtle;
+ Var *subvar;
+ ListCell *cell;
+ Form_pg_attribute attform
+ = SystemAttributeDefinition(ObjectIdAttributeNumber, true);
+
+ subrte = rt_fetch((Index) 1, query->rtable);
+ Assert(subrte->rtekind == RTE_RELATION);
+
+ if (!SearchSysCacheExists2(ATTNUM,
+ ObjectIdGetDatum(subrte->relid),
+ Int16GetDatum(attform->attnum)))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_COLUMN),
+ errmsg("table \"%s\" does not have OIDs",
+ get_rel_name(subrte->relid))));
+
+ subvar = makeVar((Index) 1,
+ attform->attnum,
+ attform->atttypid,
+ attform->atttypmod,
+ attform->attcollation,
+ 0);
+ subtle = makeTargetEntry((Expr *) subvar,
+ 0,
+ pstrdup(NameStr(attform->attname)),
+ false);
+
+ query->targetList = list_concat(list_make1(subtle),
+ query->targetList);
+ /* adjust resno of TargetEntry */
+ foreach (cell, query->targetList)
+ {
+ subtle = lfirst(cell);
+ subtle->resno++;
+ }
+}
+
+/*
* Common setup routines used by BeginCopyFrom and BeginCopyTo.
*
* Iff , unload or reload in the binary format, as opposed to the
@@ -1210,6 +1275,25 @@ BeginCopy(bool is_from,
Assert(!is_from);
cstate->rel = NULL;
+ /*
+ * In case when regular COPY TO was replaced because of row-level
+ * security, "raw_query" node have already analyzed / rewritten
+ * query tree.
+ */
+ if (IsA(raw_query, Query))
+ {
+ query = (Query *) raw_query;
+
+ Assert(query->querySource == QSRC_ROW_LEVEL_SECURITY);
+ if (cstate->oids)
+ {
+ fixup_oid_of_rls_query(query);
+ cstate->oids = false;
+ }
+ attnamelist = NIL;
+ }
+ else
+ {
/* Don't allow COPY w/ OIDs from a select */
if (cstate->oids)
ereport(ERROR,
@@ -1234,6 +1318,7 @@ BeginCopy(bool is_from,
elog(ERROR, "unexpected rewrite result");
query = (Query *) linitial(rewritten);
+ }
/* The grammar allows SELECT INTO, but we don't support that */
if (query->utilityStmt != NULL &&
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 33252a8..24756d0 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1964,8 +1964,12 @@ ExplainTargetRel(Plan *plan, Index rti, ExplainState *es)
case T_TidScan:
case T_ForeignScan:
case T_ModifyTable:
- /* Assert it's on a real relation */
- Assert(rte->rtekind == RTE_RELATION);
+ /*
+ * Assert it's on either a real relation, or a sub-query of
+ * row-level security being originated from a real relation.
+ */
+ Assert((rte->rtekind == RTE_RELATION ||
+ rte->rtekind == RTE_SUBQUERY) && OidIsValid(rte->relid));
objectname = get_rel_name(rte->relid);
if (es->verbose)
namespace = get_namespace_name(get_rel_namespace(rte->relid));
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index f88bf79..3896522 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -35,6 +35,7 @@
#include "catalog/pg_inherits_fn.h"
#include "catalog/pg_namespace.h"
#include "catalog/pg_opclass.h"
+#include "catalog/pg_rowlevelsec.h"
#include "catalog/pg_tablespace.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
@@ -2749,6 +2750,8 @@ AlterTableGetLockLevel(List *cmds)
case AT_SetTableSpace: /* must rewrite heap */
case AT_DropNotNull: /* may change some SQL plans */
case AT_SetNotNull:
+ case AT_SetRowLevelSecurity:
+ case AT_ResetRowLevelSecurity:
case AT_GenericOptions:
case AT_AlterColumnGenericOptions:
cmd_lockmode = AccessExclusiveLock;
@@ -3112,6 +3115,8 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
case AT_DropInherit: /* NO INHERIT */
case AT_AddOf: /* OF */
case AT_DropOf: /* NOT OF */
+ case AT_SetRowLevelSecurity:
+ case AT_ResetRowLevelSecurity:
ATSimplePermissions(rel, ATT_TABLE);
/* These commands never recurse */
/* No command-specific prep needed */
@@ -3391,6 +3396,12 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab, Relation rel,
case AT_DropOf:
ATExecDropOf(rel, lockmode);
break;
+ case AT_SetRowLevelSecurity:
+ SetRowLevelSecurity(rel, (Node *) cmd->def);
+ break;
+ case AT_ResetRowLevelSecurity:
+ SetRowLevelSecurity(rel, NULL);
+ break;
case AT_GenericOptions:
ATExecGenericOptions(rel, (List *) cmd->def);
break;
@@ -7552,6 +7563,22 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
Assert(defaultexpr);
break;
+ case OCLASS_ROWLEVELSEC:
+ /*
+ * A row-level security policy can depend on a column in case
+ * when the policy clause references a particular column.
+ * Due to same reason why TRIGGER ... WHEN does not support
+ * to change column's type being referenced in clause, row-
+ * level security policy also does not support it.
+ */
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot alter type of a column used in a row-level security policy"),
+ errdetail("%s depends on column \"%s\"",
+ getObjectDescription(&foundObject),
+ colName)));
+ break;
+
case OCLASS_PROC:
case OCLASS_TYPE:
case OCLASS_CAST:
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index c52f4ed..8e04c78 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -1869,8 +1869,11 @@ query_tree_walker(Query *query,
if (walker((Node *) query->targetList, context))
return true;
- if (walker((Node *) query->returningList, context))
- return true;
+ if (!(flags & QTW_IGNORE_RETURNING))
+ {
+ if (walker((Node *) query->returningList, context))
+ return true;
+ }
if (walker((Node *) query->jointree, context))
return true;
if (walker(query->setOperations, context))
@@ -2583,7 +2586,10 @@ query_tree_mutator(Query *query,
}
MUTATE(query->targetList, query->targetList, List *);
- MUTATE(query->returningList, query->returningList, List *);
+ if (!(flags & QTW_IGNORE_RETURNING))
+ MUTATE(query->returningList, query->returningList, List *);
+ else
+ query->returningList = copyObject(query->returningList);
MUTATE(query->jointree, query->jointree, FromExpr *);
MUTATE(query->setOperations, query->setOperations, Node *);
MUTATE(query->havingQual, query->havingQual, Node *);
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index b61005f..7a4ba39 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -33,6 +33,7 @@
#include "optimizer/planmain.h"
#include "optimizer/planner.h"
#include "optimizer/prep.h"
+#include "optimizer/rowlevelsec.h"
#include "optimizer/subselect.h"
#include "optimizer/tlist.h"
#include "parser/analyze.h"
@@ -167,6 +168,7 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams)
glob->lastPHId = 0;
glob->lastRowMarkId = 0;
glob->transientPlan = false;
+ glob->planUserId = InvalidOid;
/* Determine what fraction of the plan is likely to be scanned */
if (cursorOptions & CURSOR_OPT_FAST_PLAN)
@@ -244,6 +246,7 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams)
result->relationOids = glob->relationOids;
result->invalItems = glob->invalItems;
result->nParamExec = glob->nParamExec;
+ result->planUserId = glob->planUserId;
return result;
}
@@ -393,6 +396,19 @@ subquery_planner(PlannerGlobal *glob, Query *parse,
expand_inherited_tables(root);
/*
+ * Apply row-level security policy of the relation being referenced,
+ * if configured with either of built-in or extension's features.
+ * RangeTblEntry of the relation with row-level security policy shall
+ * be replaced with a RLS sub-query that has simple scan on the table
+ * with security policy qualifiers.
+ *
+ * This routine assumes PlannerInfo is already handled with
+ * expand_inherited_tables, thus, AppendRelInfo or PlanRowMark have
+ * valid information.
+ */
+ apply_row_level_security(root);
+
+ /*
* Set hasHavingQual to remember if HAVING clause is present. Needed
* because preprocess_expression will reduce a constant-true condition to
* an empty qual list ... but "HAVING TRUE" is not a semantic no-op.
@@ -842,7 +858,8 @@ inheritance_planner(PlannerInfo *root)
{
RangeTblEntry *rte = (RangeTblEntry *) lfirst(lr);
- if (rte->rtekind == RTE_SUBQUERY)
+ if (rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource != QSRC_ROW_LEVEL_SECURITY)
{
Index newrti;
diff --git a/src/backend/optimizer/prep/preptlist.c b/src/backend/optimizer/prep/preptlist.c
index 1af4e7f..9bf7566 100644
--- a/src/backend/optimizer/prep/preptlist.c
+++ b/src/backend/optimizer/prep/preptlist.c
@@ -37,7 +37,44 @@
static List *expand_targetlist(List *tlist, int command_type,
Index result_relation, List *range_table);
+/*
+ * lookup_varattno
+ *
+ * This routine returns an attribute number to reference a particular
+ * attribute. In case when the target relation is really relation,
+ * we can reference arbitrary attribute (including system column)
+ * without any translations. However, we have to translate varattno
+ * of Vat that references sub-queries being originated from regular
+ * relations with row-level security policy.
+ */
+static AttrNumber
+lookup_varattno(AttrNumber attno, Index rt_index, List *rtables)
+{
+ RangeTblEntry *rte = rt_fetch(rt_index, rtables);
+ if (rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_LEVEL_SECURITY)
+ {
+ ListCell *cell;
+
+ foreach (cell, rte->subquery->targetList)
+ {
+ TargetEntry *tle = lfirst(cell);
+ Var *var;
+
+ if (IsA(tle->expr, Const))
+ continue;
+
+ var = (Var *) tle->expr;
+ Assert(IsA(var, Var));
+
+ if (var->varattno == attno)
+ return tle->resno;
+ }
+ elog(ERROR, "invalid attno %d on the pseudo targetList", attno);
+ }
+ return attno;
+}
/*
* preprocess_targetlist
@@ -62,7 +99,8 @@ preprocess_targetlist(PlannerInfo *root, List *tlist)
{
RangeTblEntry *rte = rt_fetch(result_relation, range_table);
- if (rte->subquery != NULL || rte->relid == InvalidOid)
+ if (rte->subquery != NULL &&
+ rte->subquery->querySource != QSRC_ROW_LEVEL_SECURITY)
elog(ERROR, "subquery cannot be result relation");
}
@@ -95,7 +133,8 @@ preprocess_targetlist(PlannerInfo *root, List *tlist)
{
/* It's a regular table, so fetch its TID */
var = makeVar(rc->rti,
- SelfItemPointerAttributeNumber,
+ lookup_varattno(SelfItemPointerAttributeNumber,
+ rc->rti, range_table),
TIDOID,
-1,
InvalidOid,
@@ -111,7 +150,8 @@ preprocess_targetlist(PlannerInfo *root, List *tlist)
if (rc->isParent)
{
var = makeVar(rc->rti,
- TableOidAttributeNumber,
+ lookup_varattno(TableOidAttributeNumber,
+ rc->rti, range_table),
OIDOID,
-1,
InvalidOid,
@@ -129,7 +169,7 @@ preprocess_targetlist(PlannerInfo *root, List *tlist)
/* Not a table, so we need the whole row as a junk var */
var = makeWholeRowVar(rt_fetch(rc->rti, range_table),
rc->rti,
- 0,
+ lookup_varattno(0, rc->rti, range_table),
false);
snprintf(resname, sizeof(resname), "wholerow%u", rc->rowmarkId);
tle = makeTargetEntry((Expr *) var,
@@ -298,7 +338,9 @@ expand_targetlist(List *tlist, int command_type,
if (!att_tup->attisdropped)
{
new_expr = (Node *) makeVar(result_relation,
- attrno,
+ lookup_varattno(attrno,
+ result_relation,
+ range_table),
atttype,
atttypmod,
attcollation,
diff --git a/src/backend/optimizer/prep/prepunion.c b/src/backend/optimizer/prep/prepunion.c
index b91e9f4..5b60c16 100644
--- a/src/backend/optimizer/prep/prepunion.c
+++ b/src/backend/optimizer/prep/prepunion.c
@@ -55,6 +55,7 @@ typedef struct
{
PlannerInfo *root;
AppendRelInfo *appinfo;
+ bool in_returning;
} adjust_appendrel_attrs_context;
static Plan *recurse_set_operations(Node *setOp, PlannerInfo *root,
@@ -1594,6 +1595,7 @@ adjust_appendrel_attrs(PlannerInfo *root, Node *node, AppendRelInfo *appinfo)
context.root = root;
context.appinfo = appinfo;
+ context.in_returning = false;
/*
* Must be prepared to start with a Query or a bare expression tree.
@@ -1605,7 +1607,25 @@ adjust_appendrel_attrs(PlannerInfo *root, Node *node, AppendRelInfo *appinfo)
newnode = query_tree_mutator((Query *) node,
adjust_appendrel_attrs_mutator,
(void *) &context,
- QTW_IGNORE_RC_SUBQUERIES);
+ QTW_IGNORE_RC_SUBQUERIES |
+ QTW_IGNORE_RETURNING);
+ /*
+ * XXX - Returning clause should be handled in a special way.
+ * In case when result relation of UPDATE / DELETE has row-level
+ * security policy, its RangeTblEntry was replace by a sub-query,
+ * thus, references to system-column need to be adjusted to point
+ * pseudo-column behalf on the target system column.
+ * However, Var nodes in returning clause are exception, because
+ * its attribute number is evaluated towards the written image of
+ * the tuple being updated or deleted, not virtual tuple of the
+ * sub-query.
+ */
+ context.in_returning = true;
+ newnode->returningList =
+ (List *) expression_tree_mutator((Node *) newnode->returningList,
+ adjust_appendrel_attrs_mutator,
+ (void *) &context);
+
if (newnode->resultRelation == appinfo->parent_relid)
{
newnode->resultRelation = appinfo->child_relid;
@@ -1624,6 +1644,49 @@ adjust_appendrel_attrs(PlannerInfo *root, Node *node, AppendRelInfo *appinfo)
}
static Node *
+fixup_var_on_rls_subquery(RangeTblEntry *rte, Var *var)
+{
+ ListCell *cell;
+
+ Assert(rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_LEVEL_SECURITY);
+ /*
+ * In case when row-level security policy is applied on the referenced
+ * table, its RangeTblEntry (RTE_RELATION) is replaced with sub-query
+ * to filter out unprivileged rows of underlying relation.
+ * Even though reference to this sub-query should perform as if ones
+ * to real relations, system column has to be cared in special way
+ * due to the nature of sub-query.
+ * Target-entries that reference system columns should be added on
+ * rowlevelsec.c, so all we need to do here is looking up underlying
+ * target-list that can reference underlying system column, and fix-
+ * up varattno of the referencing Var node with resno of TargetEntry.
+ */
+ foreach (cell, rte->subquery->targetList)
+ {
+ TargetEntry *subtle = lfirst(cell);
+
+ if (IsA(subtle->expr, Var))
+ {
+ Var *subvar = (Var *) subtle->expr;
+ Var *newnode;
+
+ if (subvar->varattno == var->varattno)
+ {
+ newnode = copyObject(var);
+ newnode->varattno = subtle->resno;
+ return (Node *)newnode;
+ }
+ }
+ else
+ Assert(IsA(subtle->expr, Const));
+ }
+ elog(ERROR, "could not find pseudo column of %d in relation %s",
+ var->varattno, get_rel_name(rte->relid));
+ return NULL;
+}
+
+static Node *
adjust_appendrel_attrs_mutator(Node *node,
adjust_appendrel_attrs_context *context)
{
@@ -1664,6 +1727,14 @@ adjust_appendrel_attrs_mutator(Node *node,
*/
if (OidIsValid(appinfo->child_reltype))
{
+ Query *parse = context->root->parse;
+ RangeTblEntry *rte = rt_fetch(appinfo->child_relid,
+ parse->rtable);
+ if (!context->in_returning &&
+ rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_LEVEL_SECURITY)
+ var = (Var *)fixup_var_on_rls_subquery(rte, var);
+
Assert(var->vartype == appinfo->parent_reltype);
if (appinfo->parent_reltype != appinfo->child_reltype)
{
@@ -1708,7 +1779,16 @@ adjust_appendrel_attrs_mutator(Node *node,
return (Node *) rowexpr;
}
}
- /* system attributes don't need any other translation */
+ else
+ {
+ Query *parse = context->root->parse;
+ RangeTblEntry *rte = rt_fetch(appinfo->child_relid,
+ parse->rtable);
+ if (!context->in_returning &&
+ rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_LEVEL_SECURITY)
+ return fixup_var_on_rls_subquery(rte, var);
+ }
}
return (Node *) var;
}
diff --git a/src/backend/optimizer/util/Makefile b/src/backend/optimizer/util/Makefile
index 3b2d16b..3430689 100644
--- a/src/backend/optimizer/util/Makefile
+++ b/src/backend/optimizer/util/Makefile
@@ -13,6 +13,6 @@ top_builddir = ../../../..
include $(top_builddir)/src/Makefile.global
OBJS = clauses.o joininfo.o pathnode.o placeholder.o plancat.o predtest.o \
- relnode.o restrictinfo.o tlist.o var.o
+ relnode.o restrictinfo.o tlist.o var.o rowlevelsec.o
include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/optimizer/util/rowlevelsec.c b/src/backend/optimizer/util/rowlevelsec.c
new file mode 100644
index 0000000..1721bb5
--- /dev/null
+++ b/src/backend/optimizer/util/rowlevelsec.c
@@ -0,0 +1,685 @@
+/*
+ * optimizer/util/rowlvsec.c
+ * Row-level security support routines
+ *
+ * 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_rowlevelsec.h"
+#include "catalog/pg_type.h"
+#include "miscadmin.h"
+#include "nodes/makefuncs.h"
+#include "nodes/nodeFuncs.h"
+#include "nodes/plannodes.h"
+#include "optimizer/clauses.h"
+#include "optimizer/prep.h"
+#include "optimizer/rowlevelsec.h"
+#include "parser/parsetree.h"
+#include "rewrite/rewriteHandler.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+/* flags to pull row-level security policy */
+#define RLS_FLAG_HAS_SUBLINKS 0x0001
+
+/* hook to allow extensions to apply their own security policy */
+rowlevel_security_hook_type rowlevel_security_hook = NULL;
+
+/*
+ * make_pseudo_column
+ *
+ * It makes TargetEntry that references underlying attribute. It may be
+ * Const node of dummy NULL, not Var node, if it is already dropped.
+ */
+static TargetEntry *
+make_pseudo_column(RangeTblEntry *subrte, AttrNumber attnum)
+{
+ Expr *expr;
+ char *resname;
+
+ Assert(subrte->rtekind == RTE_RELATION && OidIsValid(subrte->relid));
+ if (attnum == InvalidAttrNumber)
+ {
+ expr = (Expr *) makeWholeRowVar(subrte, (Index) 1, 0, false);
+ resname = get_rel_name(subrte->relid);
+ }
+ else
+ {
+ HeapTuple tuple;
+ Form_pg_attribute attform;
+
+ tuple = SearchSysCache2(ATTNUM,
+ ObjectIdGetDatum(subrte->relid),
+ Int16GetDatum(attnum));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for attribute %d of relation %u",
+ attnum, subrte->relid);
+ attform = (Form_pg_attribute) GETSTRUCT(tuple);
+
+ if (attform->attisdropped)
+ {
+ char namebuf[NAMEDATALEN];
+
+ /* Insert NULL just for a placeholder of dropped column */
+ expr = (Expr *) makeConst(INT4OID,
+ -1,
+ InvalidOid,
+ sizeof(int32),
+ (Datum) 0,
+ true, /* isnull */
+ true); /* byval */
+ sprintf(namebuf, "dummy-%d", (int)attform->attnum);
+ resname = pstrdup(namebuf);
+ }
+ else
+ {
+ expr = (Expr *) makeVar((Index) 1,
+ attform->attnum,
+ attform->atttypid,
+ attform->atttypmod,
+ attform->attcollation,
+ 0);
+ resname = pstrdup(NameStr(attform->attname));
+ }
+ ReleaseSysCache(tuple);
+ }
+ return makeTargetEntry(expr, -1, resname, false);
+}
+
+/*
+ * append_pseudo_system_column
+ *
+ * It returns attribute number of pseudo-column relevant to the supplied
+ * Var-node referencing the RLS sub-query. If required attribute is not
+ * in target-list, it also adds a new pseudo-column.
+ */
+static AttrNumber
+append_pseudo_system_column(RangeTblEntry *rte, Var *var)
+{
+ Query *subqry = rte->subquery;
+ RangeTblEntry *subrte;
+ TargetEntry *subtle;
+ ListCell *cell;
+
+ Assert(rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_LEVEL_SECURITY);
+
+ foreach (cell, subqry->targetList)
+ {
+ subtle = lfirst(cell);
+
+ /*
+ * If referenced system column is already attached on the target-
+ * list of RLS sub-query, nothing to do here.
+ */
+ if (IsA(subtle->expr, Var))
+ {
+ Var *subvar = (Var *)subtle->expr;
+
+ if (var->varattno == subvar->varattno)
+ {
+ if (subtle->resjunk)
+ subtle->resjunk = false;
+ return subtle->resno;
+ }
+ }
+ }
+
+ /*
+ * Here is no target-list for the referenced system column, so append
+ * a new pseudo column on demand
+ */
+ subrte = rt_fetch((Index) 1, subqry->rtable);
+ subtle = make_pseudo_column(subrte, var->varattno);
+ subtle->resno = list_length(subqry->targetList) + 1;
+
+ subqry->targetList = lappend(subqry->targetList, subtle);
+ rte->eref->colnames = lappend(rte->eref->colnames,
+ makeString(pstrdup(subtle->resname)));
+ return subtle->resno;
+}
+
+/*
+ * fixup_varattno
+ *
+ * It recursively fixes up references to RLS sub-query, and adds pseudo-
+ * columns of underlying system columns, if necessary.
+ */
+typedef struct {
+ PlannerInfo *root;
+ int varlevelsup;
+ bool is_returning;
+} fixup_varattno_context;
+
+static bool
+fixup_varattno_walker(Node *node, fixup_varattno_context *context)
+{
+ if (node == NULL)
+ return false;
+
+ if (IsA(node, Var))
+ {
+ Var *var = (Var *) node;
+ RangeTblEntry *rte;
+ ListCell *cell;
+
+ /* Var node does not reference Query node currently we focus on */
+ if (var->varlevelsup != context->varlevelsup)
+ return false;
+
+ /*
+ * All the regular columns should already have its own pseudo
+ * column on expansion of RTE. Its resno of TargetEntry is
+ * identical with underlying attribute, so never need to fix-up
+ * varattno of Var node that references the sub-query.
+ */
+ if (var->varattno > InvalidAttrNumber)
+ return false;
+
+ rte = rt_fetch(var->varno, context->root->parse->rtable);
+ if (!context->is_returning &&
+ rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_LEVEL_SECURITY)
+ {
+ /*
+ * If this Var node is system-column or whole-row reference
+ * on RLS sub-queries, its varattno has to be adjusted to
+ * reference correct pseudo column. Pseudo column entries
+ * of them are not constructed at expansion time, we append
+ * it on demand.
+ */
+ var->varattno = append_pseudo_system_column(rte, var);
+ }
+ else if (rte->rtekind == RTE_RELATION && rte->inh)
+ {
+ /*
+ * Also, if this Var node is system-columns or whole-row
+ * reference on the parent relation of inheritance tree that
+ * includes RLS sub-queries, even though the parent relation
+ * itself was not expanded, its pseudo-column entries have to
+ * be added on the underlying child relations.
+ * However, no need to fix up varattno of Var node, because
+ * it shall be handled in prep/prepunion.c.
+ */
+ foreach (cell, context->root->append_rel_list)
+ {
+ AppendRelInfo *appinfo = lfirst(cell);
+
+ if (appinfo->parent_relid != var->varno)
+ continue;
+
+ rte = rt_fetch(appinfo->child_relid,
+ context->root->parse->rtable);
+ if (rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_LEVEL_SECURITY)
+ append_pseudo_system_column(rte, var);
+ }
+ }
+ return false;
+ }
+ else if (IsA(node, Query))
+ {
+ bool result;
+
+ context->varlevelsup++;
+ result = query_tree_walker((Query *) node,
+ fixup_varattno_walker,
+ (void *) context, 0);
+ context->varlevelsup--;
+
+ return result;
+ }
+ return expression_tree_walker(node,
+ fixup_varattno_walker,
+ (void *) context);
+}
+
+/*
+ * check_infinite_recursion
+ *
+ * It is a wrong row-level security configuration, if we try to expand
+ * the relation inside of RLS sub-query originated from same relation!
+ */
+static void
+check_infinite_recursion(PlannerInfo *root, Oid relid)
+{
+ PlannerInfo *parent = root->parent_root;
+
+ if (parent && parent->parse->querySource == QSRC_ROW_LEVEL_SECURITY)
+ {
+ RangeTblEntry *rte = rt_fetch(1, parent->parse->rtable);
+
+ Assert(rte->rtekind == RTE_RELATION && OidIsValid(rte->relid));
+
+ if (relid == rte->relid)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("infinite recursion detected for relation \"%s\"",
+ get_rel_name(relid))));
+ check_infinite_recursion(parent, relid);
+ }
+}
+
+/*
+ * fixup_plan_rowmark
+ *
+ * Push down the given PlanRowMark into RLS sub-query.
+ */
+static void
+fixup_plan_rowmark(RangeTblEntry *rte, PlanRowMark *rowmark)
+{
+ Query *subqry = rte->subquery;
+ RangeTblEntry *subrte;
+ TargetEntry *subtle;
+
+ Assert(!rowmark->isParent);
+
+ if (rowmark->markType == ROW_MARK_EXCLUSIVE ||
+ rowmark->markType == ROW_MARK_SHARE)
+ {
+ RowMarkClause *rclause = makeNode(RowMarkClause);
+
+ rclause->rti = (Index) 1;
+ if (rowmark->markType == ROW_MARK_EXCLUSIVE)
+ rclause->forUpdate = true;
+ else
+ rclause->forUpdate = false;
+ rclause->noWait = rowmark->noWait;
+ rclause->pushedDown = true;
+
+ subqry->rowMarks = lappend(subqry->rowMarks, rclause);
+ }
+ rowmark->markType = ROW_MARK_REFERENCE;
+
+ /*
+ * Add 'ctid' and 'tableoid' pseudo columns to be required for
+ * row-level locks
+ */
+ subrte = rt_fetch(1, subqry->rtable);
+
+ subtle = make_pseudo_column(subrte, SelfItemPointerAttributeNumber);
+ subtle->resno = list_length(subqry->targetList) + 1;
+ subqry->targetList = lappend(subqry->targetList, subtle);
+ rte->eref->colnames = lappend(rte->eref->colnames,
+ makeString(pstrdup(subtle->resname)));
+
+ subtle = make_pseudo_column(subrte, TableOidAttributeNumber);
+ subtle->resno = list_length(subqry->targetList) + 1;
+ subqry->targetList = lappend(subqry->targetList, subtle);
+ rte->eref->colnames = lappend(rte->eref->colnames,
+ makeString(pstrdup(subtle->resname)));
+}
+
+/*
+ * expand_rtentry_rls
+ *
+ * It replaces the supplied RangeTblEntry (should be RTE_RELATION) by RLS
+ * sub-query with configured row-level security policy.
+ * This sub-query should have pseudo-column relevant to the regular columns,
+ * but no pseudo-columns for system-column or whole-row reference without
+ * references to them.
+ */
+static void
+expand_rtentry_rls(PlannerInfo *root, Index rtindex, Expr *qual, int flags)
+{
+ Query *parse = root->parse;
+ RangeTblEntry *rte = rt_fetch(rtindex, parse->rtable);
+ Query *subqry;
+ RangeTblEntry *subrte;
+ RangeTblRef *subrtr;
+ TargetEntry *subtle;
+ HeapTuple tuple;
+ AttrNumber nattrs;
+ AttrNumber attnum;
+ List *targetList = NIL;
+ List *colNameList = NIL;
+ PlanRowMark *rowmark;
+
+ /* check recursion to prevent infinite loop */
+ check_infinite_recursion(root, rte->relid);
+
+ /* Expand views inside SubLink node */
+ if (flags & RLS_FLAG_HAS_SUBLINKS)
+ QueryRewriteExpr((Node *)qual, list_make1_oid(rte->relid));
+
+ /*
+ * Construction of sub-query
+ */
+ subqry = (Query *) makeNode(Query);
+ subqry->commandType = CMD_SELECT;
+ subqry->querySource = QSRC_ROW_LEVEL_SECURITY;
+
+ subrte = copyObject(rte);
+ subqry->rtable = list_make1(subrte);
+
+ subrtr = makeNode(RangeTblRef);
+ subrtr->rtindex = 1;
+ subqry->jointree = makeFromExpr(list_make1(subrtr), (Node *) qual);
+ if (flags & RLS_FLAG_HAS_SUBLINKS)
+ subqry->hasSubLinks = true;
+
+ /*
+ * Construct pseudo columns as TargetEntry of sub-query that
+ * references a particular regular attribute of the underlying
+ * relation.
+ */
+ tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(rte->relid));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for relation %u", rte->relid);
+ nattrs = ((Form_pg_class) GETSTRUCT(tuple))->relnatts;
+ ReleaseSysCache(tuple);
+
+ for (attnum = 1; attnum <= nattrs; attnum++)
+ {
+ subtle = make_pseudo_column(subrte, attnum);
+ subtle->resno = list_length(targetList) + 1;
+ Assert(subtle->resno == attnum);
+
+ targetList = lappend(targetList, subtle);
+ colNameList = lappend(colNameList,
+ makeString(pstrdup(subtle->resname)));
+ }
+ subqry->targetList = targetList;
+
+ /* Replace the original RengeTblEntry by sub-query */
+ /* XXX - relid has to be kept */
+ rte->rtekind = RTE_SUBQUERY;
+ rte->subquery = subqry;
+ rte->security_barrier = true;
+
+ /* no permission checks on subquery itself */
+ rte->requiredPerms = 0;
+ rte->checkAsUser = InvalidOid;
+ rte->selectedCols = NULL;
+ rte->modifiedCols = NULL;
+
+ rte->eref = makeAlias(get_rel_name(rte->relid), colNameList);
+
+ /*
+ * Push-down of PlanRowMark if needed
+ */
+ rowmark = get_plan_rowmark(root->rowMarks, rtindex);
+ if (rowmark)
+ fixup_plan_rowmark(rte, rowmark);
+}
+
+/*
+ * pull_row_level_security
+ *
+ * It pulls the configured row-level security policy of both built-in
+ * and extensions. If any, it returns expression tree.
+ */
+static Expr *
+pull_row_level_security(Relation relation, int *p_flags)
+{
+ Expr *quals = NULL;
+ int flags = 0;
+
+ /*
+ * Pull the row-level security policy configured with built-in
+ * features, if unprivileged users. Please note that superuser
+ * can bypass it.
+ */
+ if (relation->rlsdesc && !superuser())
+ {
+ RowLevelSecDesc *rlsdesc = relation->rlsdesc;
+
+ quals = copyObject(rlsdesc->rlsqual);
+ if (rlsdesc->rlshassublinks)
+ flags |= RLS_FLAG_HAS_SUBLINKS;
+ }
+
+ /*
+ * Also, ask extensions whether they want to apply their own
+ * row-level security policy. If both built-in and extension
+ * has their own policy, it shall be merged.
+ */
+ if (rowlevel_security_hook)
+ {
+ List *temp;
+
+ temp = (*rowlevel_security_hook)(relation);
+ if (temp != NIL)
+ {
+ if ((flags & RLS_FLAG_HAS_SUBLINKS) == 0 &&
+ contain_subplans((Node *) temp))
+ flags |= RLS_FLAG_HAS_SUBLINKS;
+
+ if (quals != NULL)
+ temp = lappend(temp, quals);
+
+ if (list_length(temp) == 1)
+ quals = (Expr *)list_head(temp);
+ else if (list_length(temp) > 1)
+ quals = makeBoolExpr(AND_EXPR, temp, -1);
+ }
+ }
+ *p_flags = flags;
+ return quals;
+}
+
+/*
+ * copy_row_level_security
+ *
+ * It construct a RLS sub-query instead of raw COPY TO statement,
+ * if target relation has a row-level security policy
+ */
+bool
+copy_row_level_security(CopyStmt *stmt, Relation rel, List *attnums)
+{
+ Expr *quals;
+ int flags;
+ Query *parse;
+ RangeTblEntry *rte;
+ RangeTblRef *rtr;
+ TargetEntry *tle;
+ Var *var;
+ ListCell *cell;
+
+ if (stmt->is_from)
+ return false;
+
+ quals = pull_row_level_security(rel, &flags);
+ if (!quals)
+ return false;
+
+ parse = (Query *) makeNode(Query);
+ parse->commandType = CMD_SELECT;
+ parse->querySource = QSRC_ROW_LEVEL_SECURITY;
+
+ rte = makeNode(RangeTblEntry);
+ rte->rtekind = RTE_RELATION;
+ rte->relid = RelationGetRelid(rel);
+ rte->relkind = RelationGetForm(rel)->relkind;
+
+ foreach (cell, attnums)
+ {
+ HeapTuple tuple;
+ Form_pg_attribute attform;
+ AttrNumber attno = lfirst_int(cell);
+
+ tuple = SearchSysCache2(ATTNUM,
+ ObjectIdGetDatum(RelationGetRelid(rel)),
+ Int16GetDatum(attno));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for attribute %d of relation %s",
+ attno, RelationGetRelationName(rel));
+ attform = (Form_pg_attribute) GETSTRUCT(tuple);
+
+ var = makeVar((Index) 1,
+ attform->attnum,
+ attform->atttypid,
+ attform->atttypmod,
+ attform->attcollation,
+ 0);
+ tle = makeTargetEntry((Expr *) var,
+ list_length(parse->targetList) + 1,
+ pstrdup(NameStr(attform->attname)),
+ false);
+ parse->targetList = lappend(parse->targetList, tle);
+
+ ReleaseSysCache(tuple);
+
+ rte->selectedCols = bms_add_member(rte->selectedCols,
+ attno - FirstLowInvalidHeapAttributeNumber);
+ }
+ rte->inFromCl = true;
+ rte->requiredPerms = ACL_SELECT;
+
+ rtr = makeNode(RangeTblRef);
+ rtr->rtindex = 1;
+
+ parse->jointree = makeFromExpr(list_make1(rtr), (Node *) quals);
+ parse->rtable = list_make1(rte);
+ if (flags & RLS_FLAG_HAS_SUBLINKS)
+ parse->hasSubLinks = true;
+
+ stmt->query = (Node *) parse;
+
+ return true;
+}
+
+/*
+ * apply_row_level_security
+ *
+ * Entrypoint to apply configured row-level security policy of the relation.
+ *
+ * In case when the supplied query references relations with row-level
+ * security policy, its RangeTblEntry shall be replaced by a RLS sub-query
+ * that has simple scan on the referenced table with policy qualifiers.
+ * Of course, security-barrier shall be set on the sub-query to prevent
+ * unexpected push-down of functions without leakproof flag.
+ *
+ * For example, when table t1 has a security policy "(x % 2 = 0)", the
+ * following query:
+ * SELECT * FROM t1 WHERE f_leak(y)
+ * performs as if
+ * SELECT * FROM (
+ * SELECT x, y FROM t1 WHERE (x % 2 = 0)
+ * ) AS t1 WHERE f_leak(y)
+ * would be given. Because the sub-query has security barrier flag,
+ * configured security policy qualifier is always executed prior to
+ * user given functions.
+ */
+void
+apply_row_level_security(PlannerInfo *root)
+{
+ Query *parse = root->parse;
+ Oid curr_userid;
+ int curr_seccxt;
+ Index rtindex;
+ bool has_rowlevel_security = false;
+
+ /*
+ * Mode checks. In case when SECURITY_ROW_LEVEL_DISABLED is set,
+ * no row-level security policy should be applied regardless
+ * whether it is built-in or extension.
+ */
+ GetUserIdAndSecContext(&curr_userid, &curr_seccxt);
+ if (curr_seccxt & SECURITY_ROW_LEVEL_DISABLED)
+ return;
+
+ for (rtindex = 1; rtindex <= list_length(parse->rtable); rtindex++)
+ {
+ RangeTblEntry *rte = rt_fetch(rtindex, parse->rtable);
+ Relation rel;
+ Expr *quals;
+ int flags;
+
+ /* only relation can have row-level security policy */
+ if (rte->rtekind != RTE_RELATION)
+ continue;
+
+ /*
+ * Parent relation of inheritance tree is just a placeholder here.
+ * So, no need to apply row-level security.
+ */
+ if (rte->inh)
+ continue;
+
+ /*
+ * It does not make sense to check row-level security policy on
+ * the target relation of INSERT command.
+ */
+ if (parse->commandType == CMD_INSERT &&
+ parse->resultRelation == rtindex)
+ continue;
+
+ /*
+ * It does not make sense to apply row-level security policy on
+ * the relation we already handled.
+ * Note that the underlying relation never have inh==true.
+ */
+ if (parse->querySource == QSRC_ROW_LEVEL_SECURITY &&
+ rtindex == 1)
+ continue;
+
+ /*
+ * OK, it is a reference to "real" relation. Let's try to apply
+ * row-level security policy being configured, if any.
+ */
+ rel = heap_open(rte->relid, NoLock);
+
+ quals = pull_row_level_security(rel, &flags);
+ if (quals)
+ {
+ expand_rtentry_rls(root, rtindex, quals, flags);
+ has_rowlevel_security = true;
+ }
+ heap_close(rel, NoLock);
+ }
+
+ if (has_rowlevel_security)
+ {
+ PlannerGlobal *glob = root->glob;
+ PlanInvalItem *pi_item;
+ fixup_varattno_context context;
+
+ /*
+ * XXX - Constructed Plan with row-level security policy depends
+ * on properties of current used (database superuser can bypass
+ * configured RLS policy), thus, it has to be invalidated when
+ * its assumption was changed.
+ */
+ if (!OidIsValid(glob->planUserId))
+ {
+ /* Plan invalidation on session user-id */
+ glob->planUserId = GetUserId();
+
+ /* Plan invalidation on catalog updates of pg_authid */
+ pi_item = makeNode(PlanInvalItem);
+ pi_item->cacheId = AUTHOID;
+ pi_item->hashValue =
+ GetSysCacheHashValue1(AUTHOID,
+ ObjectIdGetDatum(glob->planUserId));
+ glob->invalItems = lappend(glob->invalItems, pi_item);
+ }
+ else
+ Assert(glob->planUserId == GetUserId());
+
+ /*
+ * XXX - varattno of Var node that references the RangeTblEntry
+ * being replaced by RLS sub-query has to be adjusted for proper
+ * reference to the underlying pseudo-column of the relation.
+ */
+ context.root = root;
+ context.varlevelsup = 0;
+ context.is_returning = false;
+ query_tree_walker(parse,
+ fixup_varattno_walker,
+ (void *) &context,
+ QTW_IGNORE_RETURNING);
+ context.is_returning = true;
+ expression_tree_walker((Node *)parse->returningList,
+ fixup_varattno_walker,
+ (void *) &context);
+ }
+}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index e4ff76e..a3d445d 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -2127,6 +2127,22 @@ alter_table_cmd:
n->def = (Node *)$2;
$$ = (Node *)n;
}
+ /* ALTER TABLE SET ROW LEVEL SECURITY (expression) */
+ | SET ROW LEVEL SECURITY '(' a_expr ')'
+ {
+ AlterTableCmd *n = makeNode(AlterTableCmd);
+ n->subtype = AT_SetRowLevelSecurity;
+ n->def = (Node *) $6;
+ $$ = (Node *)n;
+ }
+ /* ALTER TABLE RESET ROW LEVEL SECURITY */
+ | RESET ROW LEVEL SECURITY
+ {
+ AlterTableCmd *n = makeNode(AlterTableCmd);
+ n->subtype = AT_ResetRowLevelSecurity;
+ n->def = NULL;
+ $$ = (Node *)n;
+ }
| alter_generic_options
{
AlterTableCmd *n = makeNode(AlterTableCmd);
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
index b75b2d9..9494889 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -269,6 +269,9 @@ transformAggregateCall(ParseState *pstate, Aggref *agg,
case EXPR_KIND_TRIGGER_WHEN:
err = _("aggregate functions are not allowed in trigger WHEN conditions");
break;
+ case EXPR_KIND_ROW_LEVEL_SEC:
+ err = _("aggregate functions are not allowed in row-level security");
+ break;
/*
* There is intentionally no default: case here, so that the
@@ -537,6 +540,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
case EXPR_KIND_TRIGGER_WHEN:
err = _("window functions are not allowed in trigger WHEN conditions");
break;
+ case EXPR_KIND_ROW_LEVEL_SEC:
+ err = _("window functions are not allowed in row-level security");
+ break;
/*
* There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index e9267c5..94c25d1 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -1443,6 +1443,7 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
case EXPR_KIND_OFFSET:
case EXPR_KIND_RETURNING:
case EXPR_KIND_VALUES:
+ case EXPR_KIND_ROW_LEVEL_SEC:
/* okay */
break;
case EXPR_KIND_CHECK_CONSTRAINT:
@@ -2609,6 +2610,8 @@ ParseExprKindName(ParseExprKind exprKind)
return "EXECUTE";
case EXPR_KIND_TRIGGER_WHEN:
return "WHEN";
+ case EXPR_KIND_ROW_LEVEL_SEC:
+ return "ROW LEVEL SECURITY";
/*
* There is intentionally no default: case here, so that the
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index b785c26..12e7dd7 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -2250,3 +2250,19 @@ QueryRewrite(Query *parsetree)
return results;
}
+
+/*
+ * QueryRewriteExpr
+ *
+ * This routine provides an entry point of query rewriter towards
+ * a certain expression tree with SubLink node; being added after
+ * the top level query rewrite.
+ * It primarily intends to expand views appeared in the qualifiers
+ * appended with row-level security which needs to modify query
+ * tree at head of the planner stage.
+ */
+void
+QueryRewriteExpr(Node *node, List *activeRIRs)
+{
+ fireRIRonSubLink(node, activeRIRs);
+}
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index 97e68b1..e54f92f 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -2999,6 +2999,7 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
int spi_result;
Oid save_userid;
int save_sec_context;
+ int temp_sec_context;
Datum vals[RI_MAX_NUMKEYS * 2];
char nulls[RI_MAX_NUMKEYS * 2];
@@ -3078,8 +3079,18 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
/* Switch to proper UID to perform check as */
GetUserIdAndSecContext(&save_userid, &save_sec_context);
+
+ /*
+ * Row-level security should be disabled in case when foreign-key
+ * relation is queried to check existence of tuples that references
+ * the primary-key being modified.
+ */
+ temp_sec_context = save_sec_context | SECURITY_LOCAL_USERID_CHANGE;
+ if (source_is_pk)
+ temp_sec_context |= SECURITY_ROW_LEVEL_DISABLED;
+
SetUserIdAndSecContext(RelationGetForm(query_rel)->relowner,
- save_sec_context | SECURITY_LOCAL_USERID_CHANGE);
+ temp_sec_context);
/* Finally we can run the query. */
spi_result = SPI_execute_snapshot(qplan,
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index 8c0391f..36a8750 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -51,6 +51,7 @@
#include "catalog/namespace.h"
#include "executor/executor.h"
#include "executor/spi.h"
+#include "miscadmin.h"
#include "nodes/nodeFuncs.h"
#include "optimizer/planmain.h"
#include "optimizer/prep.h"
@@ -665,6 +666,16 @@ CheckCachedPlan(CachedPlanSource *plansource)
AcquireExecutorLocks(plan->stmt_list, true);
/*
+ * If plan was constructed with assumption of a particular user-id,
+ * and it is different from the current one, the cached-plan shall
+ * be invalidated to construct suitable query plan.
+ */
+ if (plan->is_valid &&
+ OidIsValid(plan->planUserId) &&
+ plan->planUserId == GetUserId())
+ plan->is_valid = false;
+
+ /*
* If plan was transient, check to see if TransactionXmin has
* advanced, and if so invalidate it.
*/
@@ -716,6 +727,8 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
{
CachedPlan *plan;
List *plist;
+ ListCell *cell;
+ Oid planUserId = InvalidOid;
bool snapshot_set;
bool spi_pushed;
MemoryContext plan_context;
@@ -794,6 +807,24 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
PopOverrideSearchPath();
/*
+ * Check whether the generated plan assumes a particular user-id, or not.
+ * In case when a valid user-id is recorded on PlannedStmt->planUserId,
+ * it should be kept and used to validation check of the cached plan
+ * under the "current" user-id.
+ */
+ foreach (cell, plist)
+ {
+ PlannedStmt *pstmt = lfirst(cell);
+
+ if (IsA(pstmt, PlannedStmt) && OidIsValid(pstmt->planUserId))
+ {
+ Assert(!OidIsValid(planUserId) || planUserId == pstmt->planUserId);
+
+ planUserId = pstmt->planUserId;
+ }
+ }
+
+ /*
* Make a dedicated memory context for the CachedPlan and its subsidiary
* data. It's probably not going to be large, but just in case, use the
* default maxsize parameter. It's transient for the moment.
@@ -828,6 +859,7 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
plan->context = plan_context;
plan->is_saved = false;
plan->is_valid = true;
+ plan->planUserId = planUserId;
/* assign generation number to new plan */
plan->generation = ++(plansource->generation);
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 8c9ebe0..28ba831 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -49,6 +49,7 @@
#include "catalog/pg_opclass.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_rewrite.h"
+#include "catalog/pg_rowlevelsec.h"
#include "catalog/pg_tablespace.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
@@ -896,6 +897,11 @@ RelationBuildDesc(Oid targetRelId, bool insertIt)
else
relation->trigdesc = NULL;
+ if (relation->rd_rel->relhasrowlevelsec)
+ RelationBuildRowLevelSecurity(relation);
+ else
+ relation->rlsdesc = NULL;
+
/*
* if it's an index, initialize index-related information
*/
@@ -1785,6 +1791,8 @@ RelationDestroyRelation(Relation relation)
MemoryContextDelete(relation->rd_indexcxt);
if (relation->rd_rulescxt)
MemoryContextDelete(relation->rd_rulescxt);
+ if (relation->rlsdesc)
+ MemoryContextDelete(relation->rlsdesc->rlscxt);
pfree(relation);
}
@@ -3024,7 +3032,13 @@ RelationCacheInitializePhase3(void)
relation->rd_rel->relhastriggers = false;
restart = true;
}
-
+ if (relation->rd_rel->relhasrowlevelsec && relation->rlsdesc == NULL)
+ {
+ RelationBuildRowLevelSecurity(relation);
+ if (relation->rlsdesc == NULL)
+ relation->rd_rel->relhasrowlevelsec = false;
+ restart = true;
+ }
/* Release hold on the relation */
RelationDecrementReferenceCount(relation);
@@ -4162,6 +4176,7 @@ load_relcache_init_file(bool shared)
rel->rd_rules = NULL;
rel->rd_rulescxt = NULL;
rel->trigdesc = NULL;
+ rel->rlsdesc = NULL;
rel->rd_indexprs = NIL;
rel->rd_indpred = NIL;
rel->rd_exclops = NULL;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 82330cb..694fa95 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -3908,6 +3908,7 @@ getTables(Archive *fout, int *numTables)
int i_reloptions;
int i_toastreloptions;
int i_reloftype;
+ int i_rlsqual;
/* Make sure we are in proper schema */
selectSourceSchema(fout, "pg_catalog");
@@ -3932,7 +3933,45 @@ getTables(Archive *fout, int *numTables)
* we cannot correctly identify inherited columns, owned sequences, etc.
*/
- if (fout->remoteVersion >= 90100)
+ if (fout->remoteVersion >= 90300)
+ {
+ /*
+ * Left join to pick up dependency info linking sequences to their
+ * owning column, if any (note this dependency is AUTO as of 8.2)
+ */
+ appendPQExpBuffer(query,
+ "SELECT c.tableoid, c.oid, c.relname, "
+ "c.relacl, c.relkind, c.relnamespace, "
+ "(%s c.relowner) AS rolname, "
+ "c.relchecks, c.relhastriggers, "
+ "c.relhasindex, c.relhasrules, c.relhasoids, "
+ "c.relfrozenxid, tc.oid AS toid, "
+ "tc.relfrozenxid AS tfrozenxid, "
+ "c.relpersistence, "
+ "CASE WHEN c.reloftype <> 0 THEN c.reloftype::pg_catalog.regtype ELSE NULL END AS reloftype, "
+ "d.refobjid AS owning_tab, "
+ "d.refobjsubid AS owning_col, "
+ "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
+ "array_to_string(c.reloptions, ', ') AS reloptions, "
+ "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions, "
+ "pg_catalog.pg_get_expr(rls.rlsqual, rls.rlsrelid) AS rlsqual "
+ "FROM pg_class c "
+ "LEFT JOIN pg_depend d ON "
+ "(c.relkind = '%c' AND "
+ "d.classid = c.tableoid AND d.objid = c.oid AND "
+ "d.objsubid = 0 AND "
+ "d.refclassid = c.tableoid AND d.deptype = 'a') "
+ "LEFT JOIN pg_class tc ON (c.reltoastrelid = tc.oid) "
+ "LEFT JOIN pg_rowlevelsec rls ON (c.oid = rls.rlsrelid) "
+ "WHERE c.relkind in ('%c', '%c', '%c', '%c', '%c') "
+ "ORDER BY c.oid",
+ username_subquery,
+ RELKIND_SEQUENCE,
+ RELKIND_RELATION, RELKIND_SEQUENCE,
+ RELKIND_VIEW, RELKIND_COMPOSITE_TYPE,
+ RELKIND_FOREIGN_TABLE);
+ }
+ else if (fout->remoteVersion >= 90100)
{
/*
* Left join to pick up dependency info linking sequences to their
@@ -3952,7 +3991,8 @@ getTables(Archive *fout, int *numTables)
"d.refobjsubid AS owning_col, "
"(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
"array_to_string(c.reloptions, ', ') AS reloptions, "
- "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions "
+ "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions, "
+ "NULL as rlsqual "
"FROM pg_class c "
"LEFT JOIN pg_depend d ON "
"(c.relkind = '%c' AND "
@@ -3988,7 +4028,8 @@ getTables(Archive *fout, int *numTables)
"d.refobjsubid AS owning_col, "
"(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
"array_to_string(c.reloptions, ', ') AS reloptions, "
- "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions "
+ "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions, "
+ "NULL AS rlsqual "
"FROM pg_class c "
"LEFT JOIN pg_depend d ON "
"(c.relkind = '%c' AND "
@@ -4023,7 +4064,8 @@ getTables(Archive *fout, int *numTables)
"d.refobjsubid AS owning_col, "
"(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
"array_to_string(c.reloptions, ', ') AS reloptions, "
- "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions "
+ "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions, "
+ "NULL AS rlsqual "
"FROM pg_class c "
"LEFT JOIN pg_depend d ON "
"(c.relkind = '%c' AND "
@@ -4058,7 +4100,8 @@ getTables(Archive *fout, int *numTables)
"d.refobjsubid AS owning_col, "
"(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
"array_to_string(c.reloptions, ', ') AS reloptions, "
- "NULL AS toast_reloptions "
+ "NULL AS toast_reloptions, "
+ "NULL AS rlsqual "
"FROM pg_class c "
"LEFT JOIN pg_depend d ON "
"(c.relkind = '%c' AND "
@@ -4094,7 +4137,8 @@ getTables(Archive *fout, int *numTables)
"d.refobjsubid AS owning_col, "
"(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
"NULL AS reloptions, "
- "NULL AS toast_reloptions "
+ "NULL AS toast_reloptions, "
+ "NULL AS rlsqual "
"FROM pg_class c "
"LEFT JOIN pg_depend d ON "
"(c.relkind = '%c' AND "
@@ -4129,7 +4173,8 @@ getTables(Archive *fout, int *numTables)
"d.refobjsubid AS owning_col, "
"NULL AS reltablespace, "
"NULL AS reloptions, "
- "NULL AS toast_reloptions "
+ "NULL AS toast_reloptions, "
+ "NULL AS rlsqual "
"FROM pg_class c "
"LEFT JOIN pg_depend d ON "
"(c.relkind = '%c' AND "
@@ -4160,7 +4205,8 @@ getTables(Archive *fout, int *numTables)
"NULL::int4 AS owning_col, "
"NULL AS reltablespace, "
"NULL AS reloptions, "
- "NULL AS toast_reloptions "
+ "NULL AS toast_reloptions, "
+ "NULL AS rlsqual "
"FROM pg_class "
"WHERE relkind IN ('%c', '%c', '%c') "
"ORDER BY oid",
@@ -4186,7 +4232,8 @@ getTables(Archive *fout, int *numTables)
"NULL::int4 AS owning_col, "
"NULL AS reltablespace, "
"NULL AS reloptions, "
- "NULL AS toast_reloptions "
+ "NULL AS toast_reloptions, "
+ "NULL AS rlsqual "
"FROM pg_class "
"WHERE relkind IN ('%c', '%c', '%c') "
"ORDER BY oid",
@@ -4222,7 +4269,8 @@ getTables(Archive *fout, int *numTables)
"NULL::int4 AS owning_col, "
"NULL AS reltablespace, "
"NULL AS reloptions, "
- "NULL AS toast_reloptions "
+ "NULL AS toast_reloptions, "
+ "NULL AS rlsqual "
"FROM pg_class c "
"WHERE relkind IN ('%c', '%c') "
"ORDER BY oid",
@@ -4270,6 +4318,7 @@ getTables(Archive *fout, int *numTables)
i_reloptions = PQfnumber(res, "reloptions");
i_toastreloptions = PQfnumber(res, "toast_reloptions");
i_reloftype = PQfnumber(res, "reloftype");
+ i_rlsqual = PQfnumber(res, "rlsqual");
if (lockWaitTimeout && fout->remoteVersion >= 70300)
{
@@ -4312,6 +4361,10 @@ getTables(Archive *fout, int *numTables)
tblinfo[i].reloftype = NULL;
else
tblinfo[i].reloftype = pg_strdup(PQgetvalue(res, i, i_reloftype));
+ if (PQgetisnull(res, i, i_rlsqual))
+ tblinfo[i].rlsqual = NULL;
+ else
+ tblinfo[i].rlsqual = pg_strdup(PQgetvalue(res, i, i_rlsqual));
tblinfo[i].ncheck = atoi(PQgetvalue(res, i, i_relchecks));
if (PQgetisnull(res, i, i_owning_tab))
{
@@ -12847,6 +12900,9 @@ dumpTableSchema(Archive *fout, TableInfo *tbinfo)
}
}
}
+ if (tbinfo->rlsqual)
+ appendPQExpBuffer(q, "ALTER TABLE ONLY %s SET ROW LEVEL SECURITY %s;\n",
+ fmtId(tbinfo->dobj.name), tbinfo->rlsqual);
if (binary_upgrade)
binary_upgrade_extension_member(q, &tbinfo->dobj, labelq->data);
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 2100d43..57bd58b 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -257,6 +257,7 @@ typedef struct _tableInfo
uint32 toast_frozenxid; /* for restore toast frozen xid */
int ncheck; /* # of CHECK expressions */
char *reloftype; /* underlying type for typed table */
+ char *rlsqual; /* row-level security policy */
/* these two are set only if table is a sequence owned by a column: */
Oid owning_tab; /* OID of table owning sequence */
int owning_col; /* attr # of column owning sequence */
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index 8499768..49baa0e 100644
--- a/src/include/catalog/dependency.h
+++ b/src/include/catalog/dependency.h
@@ -147,6 +147,7 @@ typedef enum ObjectClass
OCLASS_DEFACL, /* pg_default_acl */
OCLASS_EXTENSION, /* pg_extension */
OCLASS_EVENT_TRIGGER, /* pg_event_trigger */
+ OCLASS_ROWLEVELSEC, /* pg_rowlevelsec */
MAX_OCLASS /* MUST BE LAST */
} ObjectClass;
diff --git a/src/include/catalog/indexing.h b/src/include/catalog/indexing.h
index 238fe58..a3b07e2 100644
--- a/src/include/catalog/indexing.h
+++ b/src/include/catalog/indexing.h
@@ -311,6 +311,9 @@ DECLARE_UNIQUE_INDEX(pg_extension_name_index, 3081, on pg_extension using btree(
DECLARE_UNIQUE_INDEX(pg_range_rngtypid_index, 3542, on pg_range using btree(rngtypid oid_ops));
#define RangeTypidIndexId 3542
+DECLARE_UNIQUE_INDEX(pg_rowlevelsec_relid_index, 3839, on pg_rowlevelsec using btree(rlsrelid oid_ops));
+#define RowLevelSecurityIndexId 3839
+
/* last step of initialization script: build the indexes declared above */
BUILD_INDICES
diff --git a/src/include/catalog/pg_class.h b/src/include/catalog/pg_class.h
index f83ce80..5b6e38b 100644
--- a/src/include/catalog/pg_class.h
+++ b/src/include/catalog/pg_class.h
@@ -65,6 +65,7 @@ CATALOG(pg_class,1259) BKI_BOOTSTRAP BKI_ROWTYPE_OID(83) BKI_SCHEMA_MACRO
bool relhaspkey; /* has (or has had) PRIMARY KEY index */
bool relhasrules; /* has (or has had) any rules */
bool relhastriggers; /* has (or has had) any TRIGGERs */
+ bool relhasrowlevelsec; /* has (or has had) row-level security */
bool relhassubclass; /* has (or has had) derived classes */
TransactionId relfrozenxid; /* all Xids < this are frozen in this rel */
@@ -91,7 +92,7 @@ typedef FormData_pg_class *Form_pg_class;
* ----------------
*/
-#define Natts_pg_class 27
+#define Natts_pg_class 28
#define Anum_pg_class_relname 1
#define Anum_pg_class_relnamespace 2
#define Anum_pg_class_reltype 3
@@ -115,10 +116,11 @@ typedef FormData_pg_class *Form_pg_class;
#define Anum_pg_class_relhaspkey 21
#define Anum_pg_class_relhasrules 22
#define Anum_pg_class_relhastriggers 23
-#define Anum_pg_class_relhassubclass 24
-#define Anum_pg_class_relfrozenxid 25
-#define Anum_pg_class_relacl 26
-#define Anum_pg_class_reloptions 27
+#define Anum_pg_class_relhasrowlevelsec 24
+#define Anum_pg_class_relhassubclass 25
+#define Anum_pg_class_relfrozenxid 26
+#define Anum_pg_class_relacl 27
+#define Anum_pg_class_reloptions 28
/* ----------------
* initial contents of pg_class
@@ -130,13 +132,13 @@ typedef FormData_pg_class *Form_pg_class;
*/
/* Note: "3" in the relfrozenxid column stands for FirstNormalTransactionId */
-DATA(insert OID = 1247 ( pg_type PGNSP 71 0 PGUID 0 0 0 0 0 0 0 0 f f p r 30 0 t f f f f 3 _null_ _null_ ));
+DATA(insert OID = 1247 ( pg_type PGNSP 71 0 PGUID 0 0 0 0 0 0 0 0 f f p r 30 0 t f f f f f 3 _null_ _null_ ));
DESCR("");
-DATA(insert OID = 1249 ( pg_attribute PGNSP 75 0 PGUID 0 0 0 0 0 0 0 0 f f p r 21 0 f f f f f 3 _null_ _null_ ));
+DATA(insert OID = 1249 ( pg_attribute PGNSP 75 0 PGUID 0 0 0 0 0 0 0 0 f f p r 21 0 f f f f f f 3 _null_ _null_ ));
DESCR("");
-DATA(insert OID = 1255 ( pg_proc PGNSP 81 0 PGUID 0 0 0 0 0 0 0 0 f f p r 27 0 t f f f f 3 _null_ _null_ ));
+DATA(insert OID = 1255 ( pg_proc PGNSP 81 0 PGUID 0 0 0 0 0 0 0 0 f f p r 27 0 t f f f f f 3 _null_ _null_ ));
DESCR("");
-DATA(insert OID = 1259 ( pg_class PGNSP 83 0 PGUID 0 0 0 0 0 0 0 0 f f p r 27 0 t f f f f 3 _null_ _null_ ));
+DATA(insert OID = 1259 ( pg_class PGNSP 83 0 PGUID 0 0 0 0 0 0 0 0 f f p r 28 0 t f f f f f 3 _null_ _null_ ));
DESCR("");
diff --git a/src/include/catalog/pg_rowlevelsec.h b/src/include/catalog/pg_rowlevelsec.h
new file mode 100644
index 0000000..41fcb21
--- /dev/null
+++ b/src/include/catalog/pg_rowlevelsec.h
@@ -0,0 +1,59 @@
+/*
+ * pg_rowlevelsec.h
+ * definition of the system catalog for row-level security policy
+ * (pg_rowlevelsec)
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ */
+#ifndef PG_ROWLEVELSEC_H
+#define PG_ROWLEVELSEC_H
+
+#include "catalog/genbki.h"
+#include "nodes/primnodes.h"
+#include "utils/memutils.h"
+#include "utils/relcache.h"
+
+/* ----------------
+ * pg_rowlevelsec definition. cpp turns this into
+ * typedef struct FormData_pg_rowlevelsec
+ * ----------------
+ */
+#define RowLevelSecurityRelationId 3838
+
+CATALOG(pg_rowlevelsec,3838) BKI_WITHOUT_OIDS
+{
+ Oid rlsrelid;
+#ifdef CATALOG_VARLEN
+ pg_node_tree rlsqual;
+#endif
+} FormData_pg_rowlevelsec;
+
+/* ----------------
+ * Form_pg_rowlevelsec corresponds to a pointer to a row with
+ * the format of pg_rowlevelsec relation.
+ * ----------------
+ */
+typedef FormData_pg_rowlevelsec *Form_pg_rowlevelsec;
+
+/* ----------------
+ * compiler constants for pg_rowlevelsec
+ * ----------------
+ */
+#define Natts_pg_rowlevelsec 2
+#define Anum_pg_rowlevelsec_rlsrelid 1
+#define Anum_pg_rowlevelsec_rlsqual 2
+
+typedef struct
+{
+ MemoryContext rlscxt;
+ Expr *rlsqual;
+ bool rlshassublinks;
+} RowLevelSecDesc;
+
+extern void RelationBuildRowLevelSecurity(Relation relation);
+extern void SetRowLevelSecurity(Relation relation, Node *clause);
+extern void RemoveRowLevelSecurityById(Oid relationId);
+
+#endif /* PG_ROWLEVELSEC_H */
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index 8680ac3..1c49dfa 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -21,7 +21,7 @@
/* CopyStateData is private in commands/copy.c */
typedef struct CopyStateData *CopyState;
-extern uint64 DoCopy(const CopyStmt *stmt, const char *queryString);
+extern uint64 DoCopy(CopyStmt *stmt, const char *queryString);
extern void ProcessCopyOptions(CopyState cstate, bool is_from, List *options);
extern CopyState BeginCopyFrom(Relation rel, const char *filename,
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 3ea3493..8d7a9ad 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -277,6 +277,7 @@ extern int trace_recovery(int trace_level);
/* flags to be OR'd to form sec_context */
#define SECURITY_LOCAL_USERID_CHANGE 0x0001
#define SECURITY_RESTRICTED_OPERATION 0x0002
+#define SECURITY_ROW_LEVEL_DISABLED 0x0004
extern char *DatabasePath;
diff --git a/src/include/nodes/nodeFuncs.h b/src/include/nodes/nodeFuncs.h
index e609e4b..a1d98e4 100644
--- a/src/include/nodes/nodeFuncs.h
+++ b/src/include/nodes/nodeFuncs.h
@@ -24,6 +24,7 @@
#define QTW_IGNORE_RANGE_TABLE 0x08 /* skip rangetable entirely */
#define QTW_EXAMINE_RTES 0x10 /* examine RTEs */
#define QTW_DONT_COPY_QUERY 0x20 /* do not copy top Query */
+#define QTW_IGNORE_RETURNING 0x40 /* skip returning clause */
extern Oid exprType(const Node *expr);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 8834499..a771727 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -31,7 +31,8 @@ typedef enum QuerySource
QSRC_PARSER, /* added by parse analysis (now unused) */
QSRC_INSTEAD_RULE, /* added by unconditional INSTEAD rule */
QSRC_QUAL_INSTEAD_RULE, /* added by conditional INSTEAD rule */
- QSRC_NON_INSTEAD_RULE /* added by non-INSTEAD rule */
+ QSRC_NON_INSTEAD_RULE, /* added by non-INSTEAD rule */
+ QSRC_ROW_LEVEL_SECURITY, /* added by row-level security */
} QuerySource;
/* Sort ordering options for ORDER BY and CREATE INDEX */
@@ -700,6 +701,13 @@ typedef struct RangeTblEntry
/*
* Fields valid for a plain relation RTE (else zero):
+ *
+ * XXX - Query optimizer may modify and replace RangeTblEntry on
+ * a particular relation by sub-query, but should perform as result
+ * relation of the query. In this case, relid field is used to track
+ * which relation is the sub-query originated.
+ * Right now, only row-level security feature uses this field to track
+ * the relation-id of sub-query being originated.
*/
Oid relid; /* OID of the relation */
char relkind; /* relation kind (see pg_class.relkind) */
@@ -1233,6 +1241,8 @@ typedef enum AlterTableType
AT_DropInherit, /* NO INHERIT parent */
AT_AddOf, /* OF */
AT_DropOf, /* NOT OF */
+ AT_SetRowLevelSecurity, /* SET ROW LEVEL SECURITY (...) */
+ AT_ResetRowLevelSecurity, /* RESET ROW LEVEL SECURITY */
AT_GenericOptions /* OPTIONS (...) */
} AlterTableType;
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index fb9a863..6b3ea3d 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -67,6 +67,8 @@ typedef struct PlannedStmt
List *invalItems; /* other dependencies, as PlanInvalItems */
int nParamExec; /* number of PARAM_EXEC Params used */
+
+ Oid planUserId; /* user-id this plan assumed, or InvalidOid */
} PlannedStmt;
/* macro for fetching the Plan associated with a SubPlan node */
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 0a1f8d5..b195dd7 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -98,6 +98,8 @@ typedef struct PlannerGlobal
Index lastRowMarkId; /* highest PlanRowMark ID assigned */
bool transientPlan; /* redo plan when TransactionXmin changes? */
+
+ Oid planUserId; /* User-Id to be assumed on this plan */
} PlannerGlobal;
/* macro for fetching the Plan associated with a SubPlan node */
diff --git a/src/include/optimizer/rowlevelsec.h b/src/include/optimizer/rowlevelsec.h
new file mode 100644
index 0000000..c8e0019
--- /dev/null
+++ b/src/include/optimizer/rowlevelsec.h
@@ -0,0 +1,25 @@
+/* -------------------------------------------------------------------------
+ *
+ * rowlevelsec.h
+ * prototypes for optimizer/rowlevelsec.c
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * -------------------------------------------------------------------------
+ */
+#ifndef ROWLEVELSEC_H
+#define ROWLEVELSEC_H
+
+#include "nodes/parsenodes.h"
+#include "nodes/relation.h"
+#include "utils/rel.h"
+
+typedef List *(*rowlevel_security_hook_type)(Relation relation);
+extern PGDLLIMPORT rowlevel_security_hook_type rowlevel_security_hook;
+
+extern bool copy_row_level_security(CopyStmt *stmt,
+ Relation relation, List *attnums);
+extern void apply_row_level_security(PlannerInfo *root);
+
+#endif /* ROWLEVELSEC_H */
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index aa9c648..3a1e4ea 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -62,7 +62,8 @@ typedef enum ParseExprKind
EXPR_KIND_INDEX_PREDICATE, /* index predicate */
EXPR_KIND_ALTER_COL_TRANSFORM, /* transform expr in ALTER COLUMN TYPE */
EXPR_KIND_EXECUTE_PARAMETER, /* parameter value in EXECUTE */
- EXPR_KIND_TRIGGER_WHEN /* WHEN condition in CREATE TRIGGER */
+ EXPR_KIND_TRIGGER_WHEN, /* WHEN condition in CREATE TRIGGER */
+ EXPR_KIND_ROW_LEVEL_SEC, /* policy of ROW LEVEL SECURITY */
} ParseExprKind;
diff --git a/src/include/rewrite/rewriteHandler.h b/src/include/rewrite/rewriteHandler.h
index 50625d4..d470cad 100644
--- a/src/include/rewrite/rewriteHandler.h
+++ b/src/include/rewrite/rewriteHandler.h
@@ -18,6 +18,7 @@
#include "nodes/parsenodes.h"
extern List *QueryRewrite(Query *parsetree);
+extern void QueryRewriteExpr(Node *node, List *activeRIRs);
extern void AcquireRewriteLocks(Query *parsetree, bool forUpdatePushedDown);
extern Node *build_column_default(Relation rel, int attrno);
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index 413e846..5f89028 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -115,6 +115,8 @@ typedef struct CachedPlan
* bare utility statements) */
bool is_saved; /* is CachedPlan in a long-lived context? */
bool is_valid; /* is the stmt_list currently valid? */
+ Oid planUserId; /* is user-id that is assumed on this cached
+ plan, or InvalidOid if portable for anybody */
TransactionId saved_xmin; /* if valid, replan when TransactionXmin
* changes from this value */
int generation; /* parent's generation number for this plan */
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 4669d8a..dce8463 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -18,6 +18,7 @@
#include "catalog/pg_am.h"
#include "catalog/pg_class.h"
#include "catalog/pg_index.h"
+#include "catalog/pg_rowlevelsec.h"
#include "fmgr.h"
#include "nodes/bitmapset.h"
#include "rewrite/prs2lock.h"
@@ -109,6 +110,7 @@ typedef struct RelationData
RuleLock *rd_rules; /* rewrite rules */
MemoryContext rd_rulescxt; /* private memory cxt for rd_rules, if any */
TriggerDesc *trigdesc; /* Trigger info, or NULL if rel has none */
+ RowLevelSecDesc *rlsdesc; /* Row-level security info, or NULL */
/*
* rd_options is set whenever rd_rel is loaded into the relcache entry.
diff --git a/src/test/regress/expected/rowlevelsec.out b/src/test/regress/expected/rowlevelsec.out
new file mode 100644
index 0000000..854acb0
--- /dev/null
+++ b/src/test/regress/expected/rowlevelsec.out
@@ -0,0 +1,954 @@
+--
+-- Test of Row-level security feature
+--
+-- Clean up in case a prior regression run failed
+-- Suppress NOTICE messages when users/groups don't exist
+SET client_min_messages TO 'warning';
+DROP USER IF EXISTS rls_regress_user0;
+DROP USER IF EXISTS rls_regress_user1;
+DROP USER IF EXISTS rls_regress_user2;
+DROP SCHEMA IF EXISTS rls_regress_schema CASCADE;
+RESET client_min_messages;
+-- initial setup
+CREATE USER rls_regress_user0;
+CREATE USER rls_regress_user1;
+CREATE USER rls_regress_user2;
+CREATE SCHEMA rls_regress_schema;
+GRANT ALL ON SCHEMA rls_regress_schema TO public;
+SET search_path = rls_regress_schema;
+-- setup of malicious function
+CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool
+ COST 0.0000001 LANGUAGE plpgsql
+ AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END';
+GRANT EXECUTE ON FUNCTION f_leak(text) TO public;
+-- BASIC Row-Level Security Scenario
+CREATE TABLE uaccount (
+ pguser name primary key,
+ seclv int
+);
+INSERT INTO uaccount VALUES
+ ('rls_regress_user0', 99),
+ ('rls_regress_user1', 1),
+ ('rls_regress_user2', 2),
+ ('rls_regress_user3', 3);
+GRANT SELECT ON uaccount TO public;
+SET SESSION AUTHORIZATION rls_regress_user0;
+CREATE TABLE category (
+ cid int primary key,
+ cname text
+);
+GRANT ALL ON category TO public;
+INSERT INTO category VALUES
+ (11, 'novel'),
+ (22, 'science fiction'),
+ (33, 'technology'),
+ (44, 'manga');
+CREATE TABLE document (
+ did int primary key,
+ cid int references category(cid),
+ dlevel int not null,
+ dauthor name,
+ dtitle text
+);
+GRANT ALL ON document TO public;
+INSERT INTO document VALUES
+ ( 1, 11, 1, 'rls_regress_user1', 'my first novel'),
+ ( 2, 11, 2, 'rls_regress_user1', 'my second novel'),
+ ( 3, 22, 2, 'rls_regress_user1', 'my science fiction'),
+ ( 4, 44, 1, 'rls_regress_user1', 'my first manga'),
+ ( 5, 44, 2, 'rls_regress_user1', 'my second manga'),
+ ( 6, 22, 1, 'rls_regress_user2', 'great science fiction'),
+ ( 7, 33, 2, 'rls_regress_user2', 'great technology book'),
+ ( 8, 44, 1, 'rls_regress_user2', 'great manga');
+-- user's security level must higher than or equal to document's one
+ALTER TABLE document SET ROW LEVEL SECURITY
+ (dlevel <= (SELECT seclv FROM uaccount WHERE pguser = current_user));
+-- viewpoint from rls_regress_user1
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE: f_leak => my first novel
+NOTICE: f_leak => my first manga
+NOTICE: f_leak => great science fiction
+NOTICE: f_leak => great manga
+ did | cid | dlevel | dauthor | dtitle
+-----+-----+--------+-------------------+-----------------------
+ 1 | 11 | 1 | rls_regress_user1 | my first novel
+ 4 | 44 | 1 | rls_regress_user1 | my first manga
+ 6 | 22 | 1 | rls_regress_user2 | great science fiction
+ 8 | 44 | 1 | rls_regress_user2 | great manga
+(4 rows)
+
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+NOTICE: f_leak => my first novel
+NOTICE: f_leak => my first manga
+NOTICE: f_leak => great science fiction
+NOTICE: f_leak => great manga
+ cid | did | dlevel | dauthor | dtitle | cname
+-----+-----+--------+-------------------+-----------------------+-----------------
+ 11 | 1 | 1 | rls_regress_user1 | my first novel | novel
+ 22 | 6 | 1 | rls_regress_user2 | great science fiction | science fiction
+ 44 | 8 | 1 | rls_regress_user2 | great manga | manga
+ 44 | 4 | 1 | rls_regress_user1 | my first manga | manga
+(4 rows)
+
+-- viewpoint from rls_regress_user2
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE: f_leak => my first novel
+NOTICE: f_leak => my second novel
+NOTICE: f_leak => my science fiction
+NOTICE: f_leak => my first manga
+NOTICE: f_leak => my second manga
+NOTICE: f_leak => great science fiction
+NOTICE: f_leak => great technology book
+NOTICE: f_leak => great manga
+ did | cid | dlevel | dauthor | dtitle
+-----+-----+--------+-------------------+-----------------------
+ 1 | 11 | 1 | rls_regress_user1 | my first novel
+ 2 | 11 | 2 | rls_regress_user1 | my second novel
+ 3 | 22 | 2 | rls_regress_user1 | my science fiction
+ 4 | 44 | 1 | rls_regress_user1 | my first manga
+ 5 | 44 | 2 | rls_regress_user1 | my second manga
+ 6 | 22 | 1 | rls_regress_user2 | great science fiction
+ 7 | 33 | 2 | rls_regress_user2 | great technology book
+ 8 | 44 | 1 | rls_regress_user2 | great manga
+(8 rows)
+
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+NOTICE: f_leak => my first novel
+NOTICE: f_leak => my second novel
+NOTICE: f_leak => my science fiction
+NOTICE: f_leak => my first manga
+NOTICE: f_leak => my second manga
+NOTICE: f_leak => great science fiction
+NOTICE: f_leak => great technology book
+NOTICE: f_leak => great manga
+ cid | did | dlevel | dauthor | dtitle | cname
+-----+-----+--------+-------------------+-----------------------+-----------------
+ 11 | 2 | 2 | rls_regress_user1 | my second novel | novel
+ 11 | 1 | 1 | rls_regress_user1 | my first novel | novel
+ 22 | 6 | 1 | rls_regress_user2 | great science fiction | science fiction
+ 22 | 3 | 2 | rls_regress_user1 | my science fiction | science fiction
+ 33 | 7 | 2 | rls_regress_user2 | great technology book | technology
+ 44 | 8 | 1 | rls_regress_user2 | great manga | manga
+ 44 | 5 | 2 | rls_regress_user1 | my second manga | manga
+ 44 | 4 | 1 | rls_regress_user1 | my first manga | manga
+(8 rows)
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+ QUERY PLAN
+----------------------------------------------------------
+ Subquery Scan on document
+ Filter: f_leak(document.dtitle)
+ -> Seq Scan on document document_1
+ Filter: (dlevel <= $0)
+ InitPlan 1 (returns $0)
+ -> Index Scan using uaccount_pkey on uaccount
+ Index Cond: (pguser = "current_user"())
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+ QUERY PLAN
+----------------------------------------------------------------------
+ Hash Join
+ Hash Cond: (category.cid = document.cid)
+ -> Seq Scan on category
+ -> Hash
+ -> Subquery Scan on document
+ Filter: f_leak(document.dtitle)
+ -> Seq Scan on document document_1
+ Filter: (dlevel <= $0)
+ InitPlan 1 (returns $0)
+ -> Index Scan using uaccount_pkey on uaccount
+ Index Cond: (pguser = "current_user"())
+(11 rows)
+
+-- only owner can change row-level security
+ALTER TABLE document SET ROW LEVEL SECURITY (true); -- fail
+ERROR: must be owner of relation document
+ALTER TABLE document RESET ROW LEVEL SECURITY; -- fail
+ERROR: must be owner of relation document
+SET SESSION AUTHORIZATION rls_regress_user0;
+ALTER TABLE document SET ROW LEVEL SECURITY (dauthor = current_user);
+-- viewpoint from rls_regress_user1 again
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE: f_leak => my first novel
+NOTICE: f_leak => my second novel
+NOTICE: f_leak => my science fiction
+NOTICE: f_leak => my first manga
+NOTICE: f_leak => my second manga
+ did | cid | dlevel | dauthor | dtitle
+-----+-----+--------+-------------------+--------------------
+ 1 | 11 | 1 | rls_regress_user1 | my first novel
+ 2 | 11 | 2 | rls_regress_user1 | my second novel
+ 3 | 22 | 2 | rls_regress_user1 | my science fiction
+ 4 | 44 | 1 | rls_regress_user1 | my first manga
+ 5 | 44 | 2 | rls_regress_user1 | my second manga
+(5 rows)
+
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+NOTICE: f_leak => my first novel
+NOTICE: f_leak => my second novel
+NOTICE: f_leak => my science fiction
+NOTICE: f_leak => my first manga
+NOTICE: f_leak => my second manga
+ cid | did | dlevel | dauthor | dtitle | cname
+-----+-----+--------+-------------------+--------------------+-----------------
+ 11 | 1 | 1 | rls_regress_user1 | my first novel | novel
+ 11 | 2 | 2 | rls_regress_user1 | my second novel | novel
+ 22 | 3 | 2 | rls_regress_user1 | my science fiction | science fiction
+ 44 | 4 | 1 | rls_regress_user1 | my first manga | manga
+ 44 | 5 | 2 | rls_regress_user1 | my second manga | manga
+(5 rows)
+
+-- viewpoint from rls_regress_user2 again
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE: f_leak => great science fiction
+NOTICE: f_leak => great technology book
+NOTICE: f_leak => great manga
+ did | cid | dlevel | dauthor | dtitle
+-----+-----+--------+-------------------+-----------------------
+ 6 | 22 | 1 | rls_regress_user2 | great science fiction
+ 7 | 33 | 2 | rls_regress_user2 | great technology book
+ 8 | 44 | 1 | rls_regress_user2 | great manga
+(3 rows)
+
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+NOTICE: f_leak => great science fiction
+NOTICE: f_leak => great technology book
+NOTICE: f_leak => great manga
+ cid | did | dlevel | dauthor | dtitle | cname
+-----+-----+--------+-------------------+-----------------------+-----------------
+ 22 | 6 | 1 | rls_regress_user2 | great science fiction | science fiction
+ 33 | 7 | 2 | rls_regress_user2 | great technology book | technology
+ 44 | 8 | 1 | rls_regress_user2 | great manga | manga
+(3 rows)
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+ QUERY PLAN
+----------------------------------------------
+ Subquery Scan on document
+ Filter: f_leak(document.dtitle)
+ -> Seq Scan on document document_1
+ Filter: (dauthor = "current_user"())
+(4 rows)
+
+EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+ QUERY PLAN
+----------------------------------------------------
+ Nested Loop
+ -> Subquery Scan on document
+ Filter: f_leak(document.dtitle)
+ -> Seq Scan on document document_1
+ Filter: (dauthor = "current_user"())
+ -> Index Scan using category_pkey on category
+ Index Cond: (cid = document.cid)
+(7 rows)
+
+-- interaction of FK/PK constraints
+SET SESSION AUTHORIZATION rls_regress_user0;
+ALTER TABLE category SET ROW LEVEL SECURITY
+ (CASE WHEN current_user = 'rls_regress_user1' THEN cid IN (11, 33)
+ WHEN current_user = 'rls_regress_user2' THEN cid IN (22, 44)
+ ELSE false END);
+-- cannot delete PK referenced by invisible FK
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document d full outer join category c on d.cid = c.cid;
+ did | cid | dlevel | dauthor | dtitle | cid | cname
+-----+-----+--------+-------------------+--------------------+-----+------------
+ 2 | 11 | 2 | rls_regress_user1 | my second novel | 11 | novel
+ 1 | 11 | 1 | rls_regress_user1 | my first novel | 11 | novel
+ | | | | | 33 | technology
+ 5 | 44 | 2 | rls_regress_user1 | my second manga | |
+ 4 | 44 | 1 | rls_regress_user1 | my first manga | |
+ 3 | 22 | 2 | rls_regress_user1 | my science fiction | |
+(6 rows)
+
+DELETE FROM category WHERE cid = 33; -- failed
+ERROR: update or delete on table "category" violates foreign key constraint "document_cid_fkey" on table "document"
+DETAIL: Key (cid)=(33) is still referenced from table "document".
+-- cannot insert FK referencing invisible PK
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document d full outer join category c on d.cid = c.cid;
+ did | cid | dlevel | dauthor | dtitle | cid | cname
+-----+-----+--------+-------------------+-----------------------+-----+-----------------
+ 6 | 22 | 1 | rls_regress_user2 | great science fiction | 22 | science fiction
+ 8 | 44 | 1 | rls_regress_user2 | great manga | 44 | manga
+ 7 | 33 | 2 | rls_regress_user2 | great technology book | |
+(3 rows)
+
+INSERT INTO document VALUES (10, 33, 1, current_user, 'hoge'); -- failed
+ERROR: insert or update on table "document" violates foreign key constraint "document_cid_fkey"
+DETAIL: Key (cid)=(33) is not present in table "category".
+-- database superuser can bypass RLS policy
+RESET SESSION AUTHORIZATION;
+SELECT * FROM document;
+ did | cid | dlevel | dauthor | dtitle
+-----+-----+--------+-------------------+-----------------------
+ 1 | 11 | 1 | rls_regress_user1 | my first novel
+ 2 | 11 | 2 | rls_regress_user1 | my second novel
+ 3 | 22 | 2 | rls_regress_user1 | my science fiction
+ 4 | 44 | 1 | rls_regress_user1 | my first manga
+ 5 | 44 | 2 | rls_regress_user1 | my second manga
+ 6 | 22 | 1 | rls_regress_user2 | great science fiction
+ 7 | 33 | 2 | rls_regress_user2 | great technology book
+ 8 | 44 | 1 | rls_regress_user2 | great manga
+(8 rows)
+
+SELECT * FROM category;
+ cid | cname
+-----+-----------------
+ 11 | novel
+ 22 | science fiction
+ 33 | technology
+ 44 | manga
+(4 rows)
+
+--
+-- Table inheritance and RLS policy
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+CREATE TABLE t1 (a int, junk1 text, b text) WITH OIDS;
+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);
+COPY t2 FROM stdin WITH (oids);
+CREATE TABLE t3 (c text, b text, a int) WITH OIDS;
+ALTER TABLE t3 INHERIT t1;
+COPY t3(a,b,c) FROM stdin WITH (oids);
+ALTER TABLE t1 SET ROW LEVEL SECURITY (a % 2 = 0); -- be even number
+ALTER TABLE t2 SET ROW LEVEL SECURITY (a % 2 = 1); -- be odd number
+SELECT * FROM t1;
+ a | b
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1;
+ QUERY PLAN
+-------------------------------------------
+ Result
+ -> Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+(9 rows)
+
+SELECT * FROM t1 WHERE f_leak(b);
+NOTICE: f_leak => bbb
+NOTICE: f_leak => ddd
+NOTICE: f_leak => abc
+NOTICE: f_leak => cde
+NOTICE: f_leak => xxx
+NOTICE: f_leak => yyy
+NOTICE: f_leak => zzz
+ a | b
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+ QUERY PLAN
+-------------------------------------------
+ Result
+ -> Append
+ -> Subquery Scan on t1
+ Filter: f_leak(t1.b)
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ Filter: f_leak(t2.b)
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+ Filter: f_leak(b)
+(12 rows)
+
+-- reference to system column
+SELECT oid, * FROM t1;
+ oid | a | b
+-----+---+-----
+ 102 | 2 | bbb
+ 104 | 4 | ddd
+ 201 | 1 | abc
+ 203 | 3 | cde
+ 301 | 1 | xxx
+ 302 | 2 | yyy
+ 303 | 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1;
+ QUERY PLAN
+-------------------------------------------
+ Result
+ -> Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+(9 rows)
+
+-- reference to whole-row reference
+SELECT *,t1 FROM t1;
+ a | b | t1
+---+-----+---------
+ 2 | bbb | (2,bbb)
+ 4 | ddd | (4,ddd)
+ 1 | abc | (1,abc)
+ 3 | cde | (3,cde)
+ 1 | xxx | (1,xxx)
+ 2 | yyy | (2,yyy)
+ 3 | zzz | (3,zzz)
+(7 rows)
+
+EXPLAIN (costs off) SELECT *,t1 FROM t1;
+ QUERY PLAN
+-------------------------------------------
+ Result
+ -> Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+(9 rows)
+
+-- for share/update lock
+SELECT * FROM t1 FOR SHARE;
+ a | b
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 FOR SHARE;
+ QUERY PLAN
+-------------------------------------------------------
+ LockRows
+ -> Result
+ -> Append
+ -> Subquery Scan on t1
+ -> LockRows
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ -> LockRows
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+(12 rows)
+
+SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+NOTICE: f_leak => bbb
+NOTICE: f_leak => ddd
+NOTICE: f_leak => abc
+NOTICE: f_leak => cde
+NOTICE: f_leak => xxx
+NOTICE: f_leak => yyy
+NOTICE: f_leak => zzz
+ a | b
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+ QUERY PLAN
+-------------------------------------------------------
+ LockRows
+ -> Result
+ -> Append
+ -> Subquery Scan on t1
+ Filter: f_leak(t1.b)
+ -> LockRows
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ Filter: f_leak(t2.b)
+ -> LockRows
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+ Filter: f_leak(b)
+(15 rows)
+
+--
+-- COPY TO statement
+--
+COPY t1 TO stdout;
+2 bbb
+4 ddd
+COPY t1 TO stdout WITH OIDS;
+102 2 bbb
+104 4 ddd
+COPY t2(c,b) TO stdout WITH OIDS;
+201 1.1 abc
+203 3.3 cde
+COPY (SELECT * FROM t1) TO stdout;
+2 bbb
+4 ddd
+1 abc
+3 cde
+1 xxx
+2 yyy
+3 zzz
+COPY document TO stdout WITH OIDS; -- failed (no oid column)
+ERROR: table "document" does not have OIDs
+--
+-- recursive RLS and VIEWs in policy
+--
+CREATE TABLE s1 (a int, b text);
+INSERT INTO s1 (SELECT x, md5(x::text) FROM generate_series(-10,10) x);
+CREATE TABLE s2 (x int, y text);
+INSERT INTO s2 (SELECT x, md5(x::text) FROM generate_series(-6,6) x);
+CREATE VIEW v2 AS SELECT * FROM s2 WHERE y like '%af%';
+ALTER TABLE s1 SET ROW LEVEL SECURITY
+ (a in (select x from s2 where y like '%2f%'));
+ALTER TABLE s2 SET ROW LEVEL SECURITY
+ (x in (select a from s1 where b like '%22%'));
+SELECT * FROM s1 WHERE f_leak(b); -- fail (infinite recursion)
+ERROR: infinite recursion detected for relation "s1"
+ALTER TABLE s2 SET ROW LEVEL SECURITY (x % 2 = 0);
+SELECT * FROM s1 WHERE f_leak(b); -- OK
+NOTICE: f_leak => c81e728d9d4c2f636f067f89cc14862c
+NOTICE: f_leak => a87ff679a2f3e71d9181a67b7542122c
+ a | b
+---+----------------------------------
+ 2 | c81e728d9d4c2f636f067f89cc14862c
+ 4 | a87ff679a2f3e71d9181a67b7542122c
+(2 rows)
+
+EXPLAIN SELECT * FROM only s1 WHERE f_leak(b);
+ QUERY PLAN
+---------------------------------------------------------------------------------------
+ Subquery Scan on s1 (cost=28.55..61.67 rows=205 width=36)
+ Filter: f_leak(s1.b)
+ -> Hash Join (cost=28.55..55.52 rows=615 width=36)
+ Hash Cond: (s1_1.a = s2.x)
+ -> Seq Scan on s1 s1_1 (cost=0.00..22.30 rows=1230 width=36)
+ -> Hash (cost=28.54..28.54 rows=1 width=4)
+ -> HashAggregate (cost=28.53..28.54 rows=1 width=4)
+ -> Subquery Scan on s2 (cost=0.00..28.52 rows=1 width=4)
+ Filter: (s2.y ~~ '%2f%'::text)
+ -> Seq Scan on s2 s2_1 (cost=0.00..28.45 rows=6 width=36)
+ Filter: ((x % 2) = 0)
+(11 rows)
+
+ALTER TABLE s1 SET ROW LEVEL SECURITY
+ (a in (select x from v2)); -- using VIEW in RLS policy
+SELECT * FROM s1 WHERE f_leak(b); -- OK
+NOTICE: f_leak => 0267aaf632e87a63288a08331f22c7c3
+NOTICE: f_leak => 1679091c5a880faf6fb5e6087eb1b2dc
+ a | b
+----+----------------------------------
+ -4 | 0267aaf632e87a63288a08331f22c7c3
+ 6 | 1679091c5a880faf6fb5e6087eb1b2dc
+(2 rows)
+
+EXPLAIN (COSTS OFF) SELECT * FROM s1 WHERE f_leak(b);
+ QUERY PLAN
+----------------------------------------------------------
+ Subquery Scan on s1
+ Filter: f_leak(s1.b)
+ -> Hash Join
+ Hash Cond: (s1_1.a = s2.x)
+ -> Seq Scan on s1 s1_1
+ -> Hash
+ -> HashAggregate
+ -> Subquery Scan on s2
+ Filter: (s2.y ~~ '%af%'::text)
+ -> Seq Scan on s2 s2_1
+ Filter: ((x % 2) = 0)
+(11 rows)
+
+SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+ xx | x | y
+----+----+----------------------------------
+ -6 | -6 | 596a3d04481816330f07e4f97510c28f
+ -4 | -4 | 0267aaf632e87a63288a08331f22c7c3
+ 2 | 2 | c81e728d9d4c2f636f067f89cc14862c
+(3 rows)
+
+EXPLAIN (COSTS OFF) SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+ QUERY PLAN
+--------------------------------------------------------------------
+ Subquery Scan on s2
+ Filter: (s2.y ~~ '%28%'::text)
+ -> Seq Scan on s2 s2_1
+ Filter: ((x % 2) = 0)
+ SubPlan 1
+ -> Limit
+ -> Subquery Scan on s1
+ -> Nested Loop Semi Join
+ Join Filter: (s1_1.a = s2_2.x)
+ -> Seq Scan on s1 s1_1
+ -> Materialize
+ -> Subquery Scan on s2_2
+ Filter: (s2_2.y ~~ '%af%'::text)
+ -> Seq Scan on s2 s2_3
+ Filter: ((x % 2) = 0)
+(15 rows)
+
+ALTER TABLE s2 SET ROW LEVEL SECURITY
+ (x in (select a from s1 where b like '%d2%'));
+SELECT * FROM s1 WHERE f_leak(b); -- fail (infinite recursion via view)
+ERROR: infinite recursion detected for relation "s1"
+-- prepared statement with rls_regress_user0 privilege
+PREPARE p1(int) AS SELECT * FROM t1 WHERE a <= $1;
+EXECUTE p1(2);
+ a | b
+---+-----
+ 2 | bbb
+ 1 | abc
+ 1 | xxx
+ 2 | yyy
+(4 rows)
+
+EXPLAIN (costs off) EXECUTE p1(2);
+ QUERY PLAN
+----------------------------------------------------------
+ Result
+ -> Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a <= 2) AND ((a % 2) = 0))
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a <= 2) AND ((a % 2) = 1))
+ -> Seq Scan on t3
+ Filter: (a <= 2)
+(10 rows)
+
+-- superuser is allowed to bypass RLS checks
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1 WHERE f_leak(b);
+NOTICE: f_leak => aaa
+NOTICE: f_leak => bbb
+NOTICE: f_leak => ccc
+NOTICE: f_leak => ddd
+NOTICE: f_leak => abc
+NOTICE: f_leak => bcd
+NOTICE: f_leak => cde
+NOTICE: f_leak => def
+NOTICE: f_leak => xxx
+NOTICE: f_leak => yyy
+NOTICE: f_leak => zzz
+ a | b
+---+-----
+ 1 | aaa
+ 2 | bbb
+ 3 | ccc
+ 4 | ddd
+ 1 | abc
+ 2 | bcd
+ 3 | cde
+ 4 | def
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(11 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+ QUERY PLAN
+---------------------------------
+ Result
+ -> Append
+ -> Seq Scan on t1
+ Filter: f_leak(b)
+ -> Seq Scan on t2
+ Filter: f_leak(b)
+ -> Seq Scan on t3
+ Filter: f_leak(b)
+(8 rows)
+
+-- plan cache should be invalidated
+EXECUTE p1(2);
+ a | b
+---+-----
+ 1 | aaa
+ 2 | bbb
+ 1 | abc
+ 2 | bcd
+ 1 | xxx
+ 2 | yyy
+(6 rows)
+
+EXPLAIN (costs off) EXECUTE p1(2);
+ QUERY PLAN
+--------------------------------
+ Result
+ -> Append
+ -> Seq Scan on t1
+ Filter: (a <= 2)
+ -> Seq Scan on t2
+ Filter: (a <= 2)
+ -> Seq Scan on t3
+ Filter: (a <= 2)
+(8 rows)
+
+PREPARE p2(int) AS SELECT * FROM t1 WHERE a = $1;
+EXECUTE p2(2);
+ a | b
+---+-----
+ 2 | bbb
+ 2 | bcd
+ 2 | yyy
+(3 rows)
+
+EXPLAIN (costs off) EXECUTE p2(2);
+ QUERY PLAN
+-------------------------------
+ Result
+ -> Append
+ -> Seq Scan on t1
+ Filter: (a = 2)
+ -> Seq Scan on t2
+ Filter: (a = 2)
+ -> Seq Scan on t3
+ Filter: (a = 2)
+(8 rows)
+
+-- also, case when privilege switch from superuser
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXECUTE p2(2);
+ a | b
+---+-----
+ 2 | bbb
+ 2 | yyy
+(2 rows)
+
+EXPLAIN (costs off) EXECUTE p2(2);
+ QUERY PLAN
+---------------------------------------------------------
+ Result
+ -> Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a = 2) AND ((a % 2) = 0))
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a = 2) AND ((a % 2) = 1))
+ -> Seq Scan on t3
+ Filter: (a = 2)
+(10 rows)
+
+--
+-- UPDATE / DELETE and Row-level security
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXPLAIN (costs off) UPDATE t1 SET b = b || b WHERE f_leak(b);
+ QUERY PLAN
+-------------------------------------
+ Update on t1
+ -> Subquery Scan on t1
+ Filter: f_leak(t1.b)
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ Filter: f_leak(t2.b)
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+ Filter: f_leak(b)
+(11 rows)
+
+UPDATE t1 SET b = b || b WHERE f_leak(b);
+NOTICE: f_leak => bbb
+NOTICE: f_leak => ddd
+NOTICE: f_leak => abc
+NOTICE: f_leak => cde
+NOTICE: f_leak => xxx
+NOTICE: f_leak => yyy
+NOTICE: f_leak => zzz
+EXPLAIN (costs off) UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b);
+ QUERY PLAN
+-------------------------------------
+ Update on t1
+ -> Subquery Scan on t1
+ Filter: f_leak(t1.b)
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+(5 rows)
+
+UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b);
+NOTICE: f_leak => bbbbbb
+NOTICE: f_leak => dddddd
+-- returning clause with system column
+UPDATE only t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+NOTICE: f_leak => bbbbbb_updt
+NOTICE: f_leak => dddddd_updt
+ oid | a | b | t1
+-----+---+-------------+-----------------
+ 102 | 2 | bbbbbb_updt | (2,bbbbbb_updt)
+ 104 | 4 | dddddd_updt | (4,dddddd_updt)
+(2 rows)
+
+UPDATE t1 SET b = b WHERE f_leak(b) RETURNING *;
+NOTICE: f_leak => bbbbbb_updt
+NOTICE: f_leak => dddddd_updt
+NOTICE: f_leak => abcabc
+NOTICE: f_leak => cdecde
+NOTICE: f_leak => xxxxxx
+NOTICE: f_leak => yyyyyy
+NOTICE: f_leak => zzzzzz
+ a | b
+---+-------------
+ 2 | bbbbbb_updt
+ 4 | dddddd_updt
+ 1 | abcabc
+ 3 | cdecde
+ 1 | xxxxxx
+ 2 | yyyyyy
+ 3 | zzzzzz
+(7 rows)
+
+UPDATE t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+NOTICE: f_leak => bbbbbb_updt
+NOTICE: f_leak => dddddd_updt
+NOTICE: f_leak => abcabc
+NOTICE: f_leak => cdecde
+NOTICE: f_leak => xxxxxx
+NOTICE: f_leak => yyyyyy
+NOTICE: f_leak => zzzzzz
+ oid | a | b | t1
+-----+---+-------------+-----------------
+ 102 | 2 | bbbbbb_updt | (2,bbbbbb_updt)
+ 104 | 4 | dddddd_updt | (4,dddddd_updt)
+ 201 | 1 | abcabc | (1,abcabc)
+ 203 | 3 | cdecde | (3,cdecde)
+ 301 | 1 | xxxxxx | (1,xxxxxx)
+ 302 | 2 | yyyyyy | (2,yyyyyy)
+ 303 | 3 | zzzzzz | (3,zzzzzz)
+(7 rows)
+
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1;
+ a | b
+---+-------------
+ 1 | aaa
+ 3 | ccc
+ 2 | bbbbbb_updt
+ 4 | dddddd_updt
+ 2 | bcd
+ 4 | def
+ 1 | abcabc
+ 3 | cdecde
+ 1 | xxxxxx
+ 2 | yyyyyy
+ 3 | zzzzzz
+(11 rows)
+
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXPLAIN (costs off) DELETE FROM only t1 WHERE f_leak(b);
+ QUERY PLAN
+-------------------------------------
+ Delete on t1
+ -> Subquery Scan on t1
+ Filter: f_leak(t1.b)
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+(5 rows)
+
+EXPLAIN (costs off) DELETE FROM t1 WHERE f_leak(b);
+ QUERY PLAN
+-------------------------------------
+ Delete on t1
+ -> Subquery Scan on t1
+ Filter: f_leak(t1.b)
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ Filter: f_leak(t2.b)
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+ Filter: f_leak(b)
+(11 rows)
+
+DELETE FROM only t1 WHERE f_leak(b) RETURNING oid, *, t1;
+NOTICE: f_leak => bbbbbb_updt
+NOTICE: f_leak => dddddd_updt
+ oid | a | b | t1
+-----+---+-------------+-----------------
+ 102 | 2 | bbbbbb_updt | (2,bbbbbb_updt)
+ 104 | 4 | dddddd_updt | (4,dddddd_updt)
+(2 rows)
+
+DELETE FROM t1 WHERE f_leak(b) RETURNING oid, *, t1;
+NOTICE: f_leak => abcabc
+NOTICE: f_leak => cdecde
+NOTICE: f_leak => xxxxxx
+NOTICE: f_leak => yyyyyy
+NOTICE: f_leak => zzzzzz
+ oid | a | b | t1
+-----+---+--------+------------
+ 201 | 1 | abcabc | (1,abcabc)
+ 203 | 3 | cdecde | (3,cdecde)
+ 301 | 1 | xxxxxx | (1,xxxxxx)
+ 302 | 2 | yyyyyy | (2,yyyyyy)
+ 303 | 3 | zzzzzz | (3,zzzzzz)
+(5 rows)
+
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1;
+ a | b
+---+-----
+ 1 | aaa
+ 3 | ccc
+ 2 | bcd
+ 4 | def
+(4 rows)
+
+--
+-- Clean up objects
+--
+RESET SESSION AUTHORIZATION;
+DROP SCHEMA rls_regress_schema CASCADE;
+NOTICE: drop cascades to 10 other objects
+DETAIL: drop cascades to function f_leak(text)
+drop cascades to table uaccount
+drop cascades to table category
+drop cascades to table document
+drop cascades to table t1
+drop cascades to table t2
+drop cascades to table t3
+drop cascades to table s1
+drop cascades to table s2
+drop cascades to view v2
+DROP USER rls_regress_user0;
+DROP USER rls_regress_user1;
+DROP USER rls_regress_user2;
diff --git a/src/test/regress/expected/sanity_check.out b/src/test/regress/expected/sanity_check.out
index 3f04442..ffcd8d3 100644
--- a/src/test/regress/expected/sanity_check.out
+++ b/src/test/regress/expected/sanity_check.out
@@ -120,6 +120,7 @@ SELECT relname, relhasindex
pg_proc | t
pg_range | t
pg_rewrite | t
+ pg_rowlevelsec | t
pg_seclabel | t
pg_shdepend | t
pg_shdescription | t
@@ -166,7 +167,7 @@ SELECT relname, relhasindex
timetz_tbl | f
tinterval_tbl | f
varchar_tbl | f
-(155 rows)
+(156 rows)
--
-- another sanity check: every system catalog that has OIDs should have
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 663bf8a..4c4552f 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -83,7 +83,7 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
# ----------
# Another group of parallel tests
# ----------
-test: privileges security_label collate
+test: privileges rowlevelsec security_label collate
# ----------
# Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index be789e3..eea4c3e 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -92,6 +92,7 @@ test: delete
test: namespace
test: prepared_xacts
test: privileges
+test: rowlevelsec
test: security_label
test: collate
test: misc
diff --git a/src/test/regress/sql/rowlevelsec.sql b/src/test/regress/sql/rowlevelsec.sql
new file mode 100644
index 0000000..a996b8c
--- /dev/null
+++ b/src/test/regress/sql/rowlevelsec.sql
@@ -0,0 +1,295 @@
+--
+-- Test of Row-level security feature
+--
+
+-- Clean up in case a prior regression run failed
+
+-- Suppress NOTICE messages when users/groups don't exist
+SET client_min_messages TO 'warning';
+
+DROP USER IF EXISTS rls_regress_user0;
+DROP USER IF EXISTS rls_regress_user1;
+DROP USER IF EXISTS rls_regress_user2;
+
+DROP SCHEMA IF EXISTS rls_regress_schema CASCADE;
+
+RESET client_min_messages;
+
+-- initial setup
+CREATE USER rls_regress_user0;
+CREATE USER rls_regress_user1;
+CREATE USER rls_regress_user2;
+
+CREATE SCHEMA rls_regress_schema;
+GRANT ALL ON SCHEMA rls_regress_schema TO public;
+SET search_path = rls_regress_schema;
+
+-- setup of malicious function
+CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool
+ COST 0.0000001 LANGUAGE plpgsql
+ AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END';
+GRANT EXECUTE ON FUNCTION f_leak(text) TO public;
+
+-- BASIC Row-Level Security Scenario
+CREATE TABLE uaccount (
+ pguser name primary key,
+ seclv int
+);
+INSERT INTO uaccount VALUES
+ ('rls_regress_user0', 99),
+ ('rls_regress_user1', 1),
+ ('rls_regress_user2', 2),
+ ('rls_regress_user3', 3);
+GRANT SELECT ON uaccount TO public;
+
+SET SESSION AUTHORIZATION rls_regress_user0;
+
+CREATE TABLE category (
+ cid int primary key,
+ cname text
+);
+GRANT ALL ON category TO public;
+INSERT INTO category VALUES
+ (11, 'novel'),
+ (22, 'science fiction'),
+ (33, 'technology'),
+ (44, 'manga');
+
+CREATE TABLE document (
+ did int primary key,
+ cid int references category(cid),
+ dlevel int not null,
+ dauthor name,
+ dtitle text
+);
+GRANT ALL ON document TO public;
+INSERT INTO document VALUES
+ ( 1, 11, 1, 'rls_regress_user1', 'my first novel'),
+ ( 2, 11, 2, 'rls_regress_user1', 'my second novel'),
+ ( 3, 22, 2, 'rls_regress_user1', 'my science fiction'),
+ ( 4, 44, 1, 'rls_regress_user1', 'my first manga'),
+ ( 5, 44, 2, 'rls_regress_user1', 'my second manga'),
+ ( 6, 22, 1, 'rls_regress_user2', 'great science fiction'),
+ ( 7, 33, 2, 'rls_regress_user2', 'great technology book'),
+ ( 8, 44, 1, 'rls_regress_user2', 'great manga');
+
+-- user's security level must higher than or equal to document's one
+ALTER TABLE document SET ROW LEVEL SECURITY
+ (dlevel <= (SELECT seclv FROM uaccount WHERE pguser = current_user));
+
+-- viewpoint from rls_regress_user1
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+-- viewpoint from rls_regress_user2
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+-- only owner can change row-level security
+ALTER TABLE document SET ROW LEVEL SECURITY (true); -- fail
+ALTER TABLE document RESET ROW LEVEL SECURITY; -- fail
+
+SET SESSION AUTHORIZATION rls_regress_user0;
+ALTER TABLE document SET ROW LEVEL SECURITY (dauthor = current_user);
+
+-- viewpoint from rls_regress_user1 again
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+-- viewpoint from rls_regress_user2 again
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+-- interaction of FK/PK constraints
+SET SESSION AUTHORIZATION rls_regress_user0;
+ALTER TABLE category SET ROW LEVEL SECURITY
+ (CASE WHEN current_user = 'rls_regress_user1' THEN cid IN (11, 33)
+ WHEN current_user = 'rls_regress_user2' THEN cid IN (22, 44)
+ ELSE false END);
+
+-- cannot delete PK referenced by invisible FK
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document d full outer join category c on d.cid = c.cid;
+DELETE FROM category WHERE cid = 33; -- failed
+
+-- cannot insert FK referencing invisible PK
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document d full outer join category c on d.cid = c.cid;
+INSERT INTO document VALUES (10, 33, 1, current_user, 'hoge'); -- failed
+
+-- database superuser can bypass RLS policy
+RESET SESSION AUTHORIZATION;
+SELECT * FROM document;
+SELECT * FROM category;
+
+--
+-- Table inheritance and RLS policy
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+
+CREATE TABLE t1 (a int, junk1 text, b text) WITH OIDS;
+ALTER TABLE t1 DROP COLUMN junk1; -- just a disturbing factor
+GRANT ALL ON t1 TO public;
+
+COPY t1 FROM stdin WITH (oids);
+101 1 aaa
+102 2 bbb
+103 3 ccc
+104 4 ddd
+\.
+
+CREATE TABLE t2 (c float) INHERITS (t1);
+COPY t2 FROM stdin WITH (oids);
+201 1 abc 1.1
+202 2 bcd 2.2
+203 3 cde 3.3
+204 4 def 4.4
+\.
+
+CREATE TABLE t3 (c text, b text, a int) WITH OIDS;
+ALTER TABLE t3 INHERIT t1;
+COPY t3(a,b,c) FROM stdin WITH (oids);
+301 1 xxx X
+302 2 yyy Y
+303 3 zzz Z
+\.
+
+ALTER TABLE t1 SET ROW LEVEL SECURITY (a % 2 = 0); -- be even number
+ALTER TABLE t2 SET ROW LEVEL SECURITY (a % 2 = 1); -- be odd number
+
+SELECT * FROM t1;
+EXPLAIN (costs off) SELECT * FROM t1;
+
+SELECT * FROM t1 WHERE f_leak(b);
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+
+-- reference to system column
+SELECT oid, * FROM t1;
+EXPLAIN (costs off) SELECT * FROM t1;
+
+-- reference to whole-row reference
+SELECT *,t1 FROM t1;
+EXPLAIN (costs off) SELECT *,t1 FROM t1;
+
+-- for share/update lock
+SELECT * FROM t1 FOR SHARE;
+EXPLAIN (costs off) SELECT * FROM t1 FOR SHARE;
+
+SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+
+--
+-- COPY TO statement
+--
+COPY t1 TO stdout;
+COPY t1 TO stdout WITH OIDS;
+COPY t2(c,b) TO stdout WITH OIDS;
+COPY (SELECT * FROM t1) TO stdout;
+COPY document TO stdout WITH OIDS; -- failed (no oid column)
+
+--
+-- recursive RLS and VIEWs in policy
+--
+CREATE TABLE s1 (a int, b text);
+INSERT INTO s1 (SELECT x, md5(x::text) FROM generate_series(-10,10) x);
+
+CREATE TABLE s2 (x int, y text);
+INSERT INTO s2 (SELECT x, md5(x::text) FROM generate_series(-6,6) x);
+CREATE VIEW v2 AS SELECT * FROM s2 WHERE y like '%af%';
+
+ALTER TABLE s1 SET ROW LEVEL SECURITY
+ (a in (select x from s2 where y like '%2f%'));
+
+ALTER TABLE s2 SET ROW LEVEL SECURITY
+ (x in (select a from s1 where b like '%22%'));
+
+SELECT * FROM s1 WHERE f_leak(b); -- fail (infinite recursion)
+
+ALTER TABLE s2 SET ROW LEVEL SECURITY (x % 2 = 0);
+
+SELECT * FROM s1 WHERE f_leak(b); -- OK
+EXPLAIN SELECT * FROM only s1 WHERE f_leak(b);
+
+ALTER TABLE s1 SET ROW LEVEL SECURITY
+ (a in (select x from v2)); -- using VIEW in RLS policy
+SELECT * FROM s1 WHERE f_leak(b); -- OK
+EXPLAIN (COSTS OFF) SELECT * FROM s1 WHERE f_leak(b);
+
+SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+EXPLAIN (COSTS OFF) SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+
+ALTER TABLE s2 SET ROW LEVEL SECURITY
+ (x in (select a from s1 where b like '%d2%'));
+SELECT * FROM s1 WHERE f_leak(b); -- fail (infinite recursion via view)
+
+-- prepared statement with rls_regress_user0 privilege
+PREPARE p1(int) AS SELECT * FROM t1 WHERE a <= $1;
+EXECUTE p1(2);
+EXPLAIN (costs off) EXECUTE p1(2);
+
+-- superuser is allowed to bypass RLS checks
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1 WHERE f_leak(b);
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+
+-- plan cache should be invalidated
+EXECUTE p1(2);
+EXPLAIN (costs off) EXECUTE p1(2);
+
+PREPARE p2(int) AS SELECT * FROM t1 WHERE a = $1;
+EXECUTE p2(2);
+EXPLAIN (costs off) EXECUTE p2(2);
+
+-- also, case when privilege switch from superuser
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXECUTE p2(2);
+EXPLAIN (costs off) EXECUTE p2(2);
+
+--
+-- UPDATE / DELETE and Row-level security
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXPLAIN (costs off) UPDATE t1 SET b = b || b WHERE f_leak(b);
+UPDATE t1 SET b = b || b WHERE f_leak(b);
+
+EXPLAIN (costs off) UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b);
+UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b);
+
+-- returning clause with system column
+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;
+
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1;
+
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXPLAIN (costs off) DELETE FROM only t1 WHERE f_leak(b);
+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;
+
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1;
+
+--
+-- Clean up objects
+--
+RESET SESSION AUTHORIZATION;
+
+DROP SCHEMA rls_regress_schema CASCADE;
+
+DROP USER rls_regress_user0;
+DROP USER rls_regress_user1;
+DROP USER rls_regress_user2;