+ 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..9255c45 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,19 @@ 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,
+ however, unavailable to include sub-query right now.
+
+
+
+
diff --git a/doc/src/sgml/user-manag.sgml b/doc/src/sgml/user-manag.sgml
index 177ac7a..c283e07 100644
--- a/doc/src/sgml/user-manag.sgml
+++ b/doc/src/sgml/user-manag.sgml
@@ -439,4 +439,127 @@ 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
+ 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
+ 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
+ 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.
+
+
+
+ Right now, row-level security feature has several limitation,
+ although these shall be improved in the future version.
+
+ Row-level security policy is not applied to
+ UPDATE or DELETE
+ commands in this revision, even though it should be
+ supported soon.
+
+ Row-level security policy is not applied to rows to be inserted
+ and newer revision of updated rows, thus, it requires to
+ define before-row-insert or before-row-update trigger to check
+ whether the row's contents satisfies the policy individually.
+
+ Although it is not a specific matter in row-level security,
+ and
+ allows to switch current user identifier during execution
+ of the query. Thus, it may cause unpredicated behavior
+ if and when current_user is used as
+ a part of row-level security policy.
+
+
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index e7a6606..851f9db 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -35,6 +35,7 @@
#include "executor/spi.h"
#include "libpq/be-fsstubs.h"
#include "miscadmin.h"
+#include "optimizer/rowlevelsec.h"
#include "pgstat.h"
#include "replication/walsender.h"
#include "replication/syncrep.h"
@@ -144,6 +145,7 @@ typedef struct TransactionStateData
int maxChildXids; /* allocated size of childXids[] */
Oid prevUser; /* previous CurrentUserId setting */
int prevSecContext; /* previous SecurityRestrictionContext */
+ RowLevelSecMode prevRowLevelSecMode; /* previous RLS-mode setting */
bool prevXactReadOnly; /* entry-time xact r/o state */
bool startedInRecovery; /* did we start in recovery? */
struct TransactionStateData *parent; /* back link to parent */
@@ -173,6 +175,7 @@ static TransactionStateData TopTransactionStateData = {
0, /* allocated size of childXids[] */
InvalidOid, /* previous CurrentUserId setting */
0, /* previous SecurityRestrictionContext */
+ RowLevelSecModeEnabled, /* previous RowLevelSecMode setting */
false, /* entry-time xact r/o state */
false, /* startedInRecovery */
NULL /* link to parent state block */
@@ -1764,6 +1767,8 @@ StartTransaction(void)
/* SecurityRestrictionContext should never be set outside a transaction */
Assert(s->prevSecContext == 0);
+ s->prevRowLevelSecMode = getRowLevelSecurityMode();
+
/*
* initialize other subsystems for new transaction
*/
@@ -2295,6 +2300,9 @@ AbortTransaction(void)
*/
SetUserIdAndSecContext(s->prevUser, s->prevSecContext);
+ /* Also, mode setting of row-level security should be restored. */
+ setRowLevelSecurityMode(s->prevRowLevelSecMode);
+
/*
* do abort processing
*/
@@ -4188,6 +4196,9 @@ AbortSubTransaction(void)
*/
SetUserIdAndSecContext(s->prevUser, s->prevSecContext);
+ /* Reset row-level security mode */
+ setRowLevelSecurityMode(s->prevRowLevelSecMode);
+
/*
* We can skip all this stuff if the subxact failed before creating a
* ResourceOwner...
@@ -4327,6 +4338,7 @@ PushTransaction(void)
s->state = TRANS_DEFAULT;
s->blockState = TBLOCK_SUBBEGIN;
GetUserIdAndSecContext(&s->prevUser, &s->prevSecContext);
+ s->prevRowLevelSecMode = getRowLevelSecurityMode();
s->prevXactReadOnly = XactReadOnly;
CurrentTransactionState = s;
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 87d6f02..f0736b1 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"
@@ -1229,6 +1230,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);
@@ -2280,6 +2285,9 @@ getObjectClass(const ObjectAddress *object)
case EventTriggerRelationId:
return OCLASS_EVENT_TRIGGER;
+
+ case RowLevelSecurityRelationId:
+ return OCLASS_ROWLEVELSEC;
}
/* shouldn't get here */
@@ -2929,6 +2937,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 c80df41..60de76d 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..e554f20
--- /dev/null
+++ b/src/backend/catalog/pg_rowlevelsec.c
@@ -0,0 +1,241 @@
+/* -------------------------------------------------------------------------
+ *
+ * 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"
+
+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);
+}
+
+void
+SetRowLevelSecurity(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 Indert 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);
+}
+
+void
+ResetRowLevelSecurity(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);
+}
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 359d478..83ca86b 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"
@@ -384,6 +385,7 @@ static void ATExecDropInherit(Relation rel, RangeVar *parent, LOCKMODE lockmode)
static void drop_parent_dependency(Oid relid, Oid refclassid, Oid refobjid);
static void ATExecAddOf(Relation rel, const TypeName *ofTypename, LOCKMODE lockmode);
static void ATExecDropOf(Relation rel, LOCKMODE lockmode);
+static void ATExecSetRowLevelSecurity(Relation relation, Node *clause);
static void ATExecGenericOptions(Relation rel, List *options);
static void copy_relation_data(SMgrRelation rel, SMgrRelation dst,
@@ -2746,6 +2748,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;
@@ -3108,6 +3112,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 */
@@ -3383,6 +3389,12 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab, Relation rel,
case AT_DropOf:
ATExecDropOf(rel, lockmode);
break;
+ case AT_SetRowLevelSecurity:
+ ATExecSetRowLevelSecurity(rel, (Node *) cmd->def);
+ break;
+ case AT_ResetRowLevelSecurity:
+ ATExecSetRowLevelSecurity(rel, NULL);
+ break;
case AT_GenericOptions:
ATExecGenericOptions(rel, (List *) cmd->def);
break;
@@ -7533,6 +7545,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:
@@ -9631,6 +9659,42 @@ ATExecDropOf(Relation rel, LOCKMODE lockmode)
}
/*
+ * ALTER TABLE SET ROW LEVEL SECURITY (...) OR
+ * RESET ROW LEVEL SECURITY
+ */
+static void
+ATExecSetRowLevelSecurity(Relation relation, Node *clause)
+{
+ Oid relid = RelationGetRelid(relation);
+ Relation class_rel;
+ HeapTuple tuple;
+ Form_pg_class class_form;
+
+ if (clause != NULL)
+ SetRowLevelSecurity(relation, clause);
+ else
+ ResetRowLevelSecurity(relation);
+
+ 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)
+ class_form->relhasrowlevelsec = true;
+ else
+ 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);
+}
+
+/*
* ALTER FOREIGN TABLE OPTIONS (...)
*/
static void
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index b61005f..ad8b9ad 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,16 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams)
glob->lastPHId = 0;
glob->lastRowMarkId = 0;
glob->transientPlan = false;
+ glob->planUserId = InvalidOid;
+ /*
+ * XXX - a valid user-id shall be set on planUserId later, if constructed
+ * plan assumes being executed under privilege of a particular user-id.
+ * Elsewhere, keep InvalidOid; that means the constructed plan is portable
+ * for any users.
+ * Please also note that it is also the reason why row-level security
+ * needs to be placed on top of planner stage, instead of rewriter.
+ */
+ applyRowLevelSecurity(glob, parse);
/* Determine what fraction of the plan is likely to be scanned */
if (cursorOptions & CURSOR_OPT_FAST_PLAN)
@@ -244,6 +255,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;
}
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..664c993
--- /dev/null
+++ b/src/backend/optimizer/util/rowlevelsec.c
@@ -0,0 +1,977 @@
+/*
+ * 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;
+
+/* current performing mode of row-level security policy */
+static RowLevelSecMode rowlevel_security_mode = RowLevelSecModeEnabled;
+
+static bool apply_subquery_rowlevelsec(Query *parse, List *active_relids);
+
+/*
+ * getRowLevelSecurityMode / setRowLevelSecurityMode
+ *
+ * These functions allow to get or set current performing mode of row-
+ * level security feature. It enables to disable this feature temporarily
+ * for some cases in which row-level security prevent correct behavior
+ * such as foreign-key checks to prohibit update of PKs being referenced
+ * by others.
+ * The caller must ensure the saved previous mode shall be restored, but
+ * no need to care about cases when an error would be raised.
+ */
+RowLevelSecMode
+getRowLevelSecurityMode(void)
+{
+ return rowlevel_security_mode;
+}
+
+void
+setRowLevelSecurityMode(RowLevelSecMode new_mode)
+{
+ rowlevel_security_mode = new_mode;
+}
+
+/*
+ * check_and_fixup_sublink
+ *
+ * This routine calls query rewriter to handle sub-query of sub-link
+ * within row-level security policy, and ensure it does not contain
+ * any infinite reference loop.
+ */
+typedef struct {
+ bool has_rowlevel_sec;
+ bool exec_query_rewriter;
+ List *active_relids;
+} fixup_sublink_context;
+
+static bool
+fixup_sublink_walker(Node *node, fixup_sublink_context *context)
+{
+ if (node == NULL)
+ return false;
+
+ if (IsA(node, SubLink))
+ {
+ SubLink *sl = (SubLink *) node;
+
+ Assert(IsA(sl->subselect, Query));
+
+ if (context->exec_query_rewriter)
+ QueryRewriteExpr((Node *) sl, context->active_relids);
+
+ if (apply_subquery_rowlevelsec((Query *)sl->subselect,
+ context->active_relids))
+ context->has_rowlevel_sec = true;
+ }
+ return expression_tree_walker(node, fixup_sublink_walker,
+ (void *) context);
+}
+
+/*
+ * make_pseudo_subquery
+ *
+ * This routine makes a sub-query that references the target relation
+ * with given row-level security policy. This sub-query shall have
+ * security_barrier attribute to prevent unexpected push-down.
+ */
+static Query *
+make_pseudo_subquery(RangeTblEntry *rte,
+ Oid relid,
+ Node *qual,
+ int flags,
+ RowMarkClause *rclause)
+{
+ Query *subqry;
+ RangeTblEntry *subrte;
+ RangeTblRef *subrtr;
+
+ subqry = makeNode(Query);
+ subqry->commandType = CMD_SELECT;
+ subqry->querySource = QSRC_ROW_LEVEL_SECURITY;
+
+ subrte = makeNode(RangeTblEntry);
+ subrte->rtekind = RTE_RELATION;
+ subrte->relid = relid;
+ subrte->relkind = get_rel_relkind(relid);
+ subrte->inFromCl = true;
+ subrte->requiredPerms = 0;
+ if (rte->relid == relid)
+ {
+ subrte->requiredPerms = rte->requiredPerms;
+ subrte->selectedCols = bms_copy(rte->selectedCols);
+ subrte->modifiedCols = bms_copy(rte->modifiedCols);
+ }
+ else
+ {
+ /*
+ * selectedCols and modifiedCols should be adjusted
+ * according to child table definition, because it
+ * may have original attribute-number in comparison
+ * with the parent table.
+ */
+ AttrNumber attidx;
+ AttrNumber attnum;
+ char *attname;
+ Bitmapset *attrs_used = bms_union(rte->selectedCols,
+ rte->modifiedCols);
+ while ((attidx = bms_first_member(attrs_used)) >= 0)
+ {
+ attnum = attidx + FirstLowInvalidHeapAttributeNumber;
+ if (attnum != InvalidAttrNumber)
+ {
+ attname = get_relid_attribute_name(rte->relid, attnum);
+ attnum = get_attnum(relid, attname);
+ if (attnum == InvalidAttrNumber)
+ elog(ERROR, "Not exist column \"%s\" of relation \"%s\"",
+ attname, get_rel_name(relid));
+ pfree(attname);
+ }
+ attnum -= FirstLowInvalidHeapAttributeNumber;
+ if (bms_is_member(attidx, rte->selectedCols))
+ bms_add_member(subrte->selectedCols, attnum);
+ if (bms_is_member(attidx, rte->modifiedCols))
+ bms_add_member(subrte->modifiedCols, attnum);
+ }
+ }
+ subrte->alias = makeAlias(get_rel_name(relid), NIL);
+ subrte->eref = makeAlias(get_rel_name(relid), NIL);
+
+ if (flags & RLS_FLAG_HAS_SUBLINKS)
+ subqry->hasSubLinks = true;
+
+ subqry->rtable = list_make1(subrte);
+
+ subrtr = makeNode(RangeTblRef);
+ subrtr->rtindex = 1;
+ subqry->jointree = makeFromExpr(list_make1(subrtr), qual);
+
+ if (rclause)
+ {
+ RowMarkClause *rclause_sub;
+
+ rclause_sub = copyObject(rclause);
+ rclause_sub->rti = 1;
+
+ subqry->rowMarks = list_make1(rclause_sub);
+ subqry->hasForUpdate = true;
+ }
+ return subqry;
+}
+
+/*
+ * append_pseudo_column
+ *
+ * this routine construct a TargetEntry object that reference a certain
+ * column of the underlying table, then append it on targetList of the
+ * given RangeTblEntry (should be RTE_SUBQUERY).
+ */
+static TargetEntry *
+append_pseudo_column(RangeTblEntry *rte, Oid base_relid, AttrNumber attnum)
+{
+ Query *subqry;
+ RangeTblEntry *subrte;
+ TargetEntry *subtle;
+ Value *colname;
+
+ Assert(rte->rtekind == RTE_SUBQUERY);
+ Assert(list_length(rte->subquery->rtable) == 1);
+ Assert(rt_fetch(1, rte->subquery->rtable)->rtekind == RTE_RELATION);
+
+ subqry = rte->subquery;
+ subrte = rt_fetch(1, subqry->rtable);
+
+ /* Special case for whole-row-reference is required */
+ if (attnum == InvalidAttrNumber)
+ {
+ ConvertRowtypeExpr *r = makeNode(ConvertRowtypeExpr);
+
+ r->arg = (Expr *) makeVar((Index) 1,
+ InvalidAttrNumber,
+ get_rel_type_id(subrte->relid),
+ -1,
+ InvalidOid,
+ 0);
+ r->resulttype = get_rel_type_id(base_relid);
+ r->convertformat = COERCE_IMPLICIT_CAST;
+ r->location = -1;
+
+ subtle = makeTargetEntry((Expr *) r,
+ list_length(subqry->targetList) + 1,
+ get_rel_name(subrte->relid),
+ false);
+ }
+ else
+ {
+ Form_pg_attribute attform;
+ HeapTuple tuple;
+ Var *var;
+
+ /*
+ * If the TargetEntry is not for base relaion, we need to
+ * adjust attribute number to be used.
+ */
+ if (attnum > 0 && subrte->relid != base_relid)
+ {
+ char *attname = get_relid_attribute_name(base_relid, attnum);
+
+ attnum = get_attnum(subrte->relid, attname);
+ if (attnum == InvalidAttrNumber)
+ elog(ERROR, "column \"%s\" of relation \"%s\" does not exist",
+ attname, get_rel_name(subrte->relid));
+ pfree(attname);
+ }
+
+ tuple = SearchSysCache2(ATTNUM,
+ ObjectIdGetDatum(subrte->relid),
+ Int16GetDatum(attnum));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for column %d of relation %u",
+ attnum, subrte->relid);
+ attform = (Form_pg_attribute) GETSTRUCT(tuple);
+
+ var = makeVar((Index) 1,
+ attform->attnum,
+ attform->atttypid,
+ attform->atttypmod,
+ attform->attcollation,
+ 0);
+ subtle = makeTargetEntry((Expr *)var,
+ list_length(subqry->targetList) + 1,
+ pstrdup(NameStr(attform->attname)),
+ false);
+ ReleaseSysCache(tuple);
+ }
+ subqry->targetList = lappend(subqry->targetList, subtle);
+
+ colname = makeString(pstrdup(subtle->resname));
+ rte->eref->colnames = lappend(rte->eref->colnames, colname);
+
+ return subtle;
+}
+
+/*
+ * lookup_pseudo_column
+ *
+ * This routine looks up attribute number of pseudo column being associated
+ * with the given attribute number of the underlying table.
+ * If not in target-list of the sub-query, it append a new target-entry.
+ */
+static AttrNumber
+lookup_pseudo_column(RangeTblEntry *rte, AttrNumber attnum)
+{
+ Query *subqry = rte->subquery;
+ ListCell *cell;
+ TargetEntry *subtle;
+ Oid base_relid;
+
+ Assert(rte->rtekind == RTE_SUBQUERY);
+ Assert(list_length(subqry->rtable) == 1);
+ Assert(rt_fetch(1, subqry->rtable)->rtekind == RTE_RELATION);
+ base_relid = rt_fetch(1, subqry->rtable)->relid;
+
+ foreach (cell, subqry->targetList)
+ {
+ subtle = lfirst(cell);
+
+ if ((IsA(subtle->expr, ConvertRowtypeExpr) &&
+ attnum == InvalidAttrNumber) ||
+ (IsA(subtle->expr, Var) &&
+ attnum == ((Var *)subtle->expr)->varattno))
+ return subtle->resno;
+ }
+ subtle = append_pseudo_column(rte, base_relid, attnum);
+
+ return subtle->resno;
+}
+
+/*
+ * fixup_setop_walker
+ *
+ * it appends information about newly added pseudo column on
+ * SetOperationStmt tree of the pseudo sub-query.
+ */
+static void
+fixup_setop_walker(Node *setop, Oid coltypeid, int coltypemod)
+{
+ if (IsA(setop, RangeTblRef))
+ return;
+ else if (IsA(setop, SetOperationStmt))
+ {
+ SetOperationStmt *s = (SetOperationStmt *) setop;
+
+ s->colTypes = lappend_oid(s->colTypes, coltypeid);
+ s->colTypmods = lappend_oid(s->colTypmods, coltypemod);
+ s->colCollations = lappend_oid(s->colCollations, InvalidOid);
+ }
+ else
+ elog(ERROR, "unexpected node type: %d", (int) nodeTag(setop));
+}
+
+/*
+ * lookup_pseudo_column_multi
+ *
+ * This routine looks up attribute number of pseudo column being associated
+ * with the given attribute number of the underlying table.
+ * If not in target-list of the sub-query, it append a new target-entry.
+ * Unlike lookup_pseudo_column, it assumes the given RangeTblEntry has
+ * multiple sub-queries underlying simple table scan.
+ */
+static AttrNumber
+lookup_pseudo_column_multi(RangeTblEntry *rte, AttrNumber attnum)
+{
+ Query *subqry;
+ RangeTblEntry *subrte;
+ TargetEntry *subtle;
+ Var *subvar;
+ ListCell *cell;
+ Value *colname;
+ Oid base_relid;
+ Oid coltypeid = InvalidOid;
+ int coltypemod = -1;
+ AttrNumber resno = -1;
+ char *resname = NULL;
+
+ Assert(rte->rtekind == RTE_SUBQUERY);
+ Assert(list_length(rte->subquery->rtable) > 1);
+
+ /*
+ * First, it looks up target-list of the given sub-query.
+ */
+ subrte = rt_fetch(1, rte->subquery->rtable);
+ Assert(subrte->rtekind == RTE_SUBQUERY);
+
+ subqry = subrte->subquery;
+ Assert(list_length(subqry->rtable) == 1);
+ Assert(rt_fetch(1, subqry->rtable)->rtekind == RTE_RELATION);
+ base_relid = rt_fetch(1, subqry->rtable)->relid;
+
+ foreach (cell, rte->subquery->targetList)
+ {
+ TargetEntry *tle = lfirst(cell);
+ Var *var = (Var *)tle->expr;
+
+ Assert(IsA(var, Var));
+ Assert(var->varattno <= list_length(subqry->targetList));
+
+ subtle = list_nth(subqry->targetList, var->varattno - 1);
+ if ((IsA(subtle->expr, ConvertRowtypeExpr) &&
+ attnum == InvalidAttrNumber) ||
+ (IsA(subtle->expr, Var) &&
+ attnum == ((Var *)subtle->expr)->varattno))
+ return tle->resno;
+ }
+
+ /*
+ * Second, append a TargetEntry to reference the required attribute
+ * of the underlying tables. Note that the underlying tables are
+ * connected with UNION ALL, target entries must have exactly same
+ * resource number and data type.
+ */
+ foreach (cell, rte->subquery->rtable)
+ {
+ subrte = (RangeTblEntry *) lfirst(cell);
+ subtle = append_pseudo_column(subrte, base_relid, attnum);
+
+ if (cell == list_head(rte->subquery->rtable))
+ {
+ resno = subtle->resno;
+ resname = pstrdup(subtle->resname);
+
+ if (IsA(subtle->expr, ConvertRowtypeExpr))
+ {
+ ConvertRowtypeExpr *r = (ConvertRowtypeExpr *) subtle->expr;
+
+ coltypeid = r->resulttype;
+ coltypemod = -1;
+ }
+ else if (IsA(subtle->expr, Var))
+ {
+ Var *v = (Var *) subtle->expr;
+
+ coltypeid = v->vartype;
+ coltypemod = v->vartypmod;
+ }
+ else
+ elog(ERROR, "unexpected node type: %d",
+ (int)nodeTag(subtle->expr));
+ }
+ else
+ Assert(subtle->resno == resno);
+ }
+ /* fixup SetOperationStmt */
+ fixup_setop_walker(rte->subquery->setOperations, coltypeid, coltypemod);
+
+ /*
+ * Not only underlying sub-queries, the base sub-qury also need to
+ * have Var entry within the target list.
+ */
+ subvar = makeVar((Index) 1,
+ resno,
+ coltypeid,
+ coltypemod,
+ InvalidOid,
+ 0);
+ subtle = makeTargetEntry((Expr *)subvar, resno, resname, false);
+ rte->subquery->targetList = lappend(rte->subquery->targetList, subtle);
+ colname = makeString(pstrdup(resname));
+ rte->eref->colnames = lappend(rte->eref->colnames, colname);
+
+ Assert(list_length(rte->subquery->targetList) == resno);
+
+ return resno;
+}
+
+/*
+ * fixup_varattnos
+ *
+ * It fixes up varattno of Var node that referenced the relation with
+ * RLS policy, thus replaced to a sub-query. Here is no guarantee the
+ * varattno matches with TargetEntry's resno of the sub-query, so needs
+ * to adjust them.
+ */
+typedef struct {
+ Query *parse;
+ Index varno;
+ int varlevelsup;
+} fixup_varattnos_context;
+
+static bool
+fixup_varattnos_walker(Node *node, fixup_varattnos_context *context)
+{
+ if (node == NULL)
+ return false;
+
+ if (IsA(node, Var))
+ {
+ Var *var = (Var *)node;
+ RangeTblEntry *rte;
+
+ /*
+ * Does this Var node reference the Query node currently we focused
+ * on. If not, we simply ignore it.
+ */
+ if (var->varlevelsup != context->varlevelsup ||
+ var->varno != context->varno)
+ return false;
+
+ rte = rt_fetch(var->varno, context->parse->rtable);
+ if (!rte)
+ elog(ERROR, "invalid varno %d", var->varno);
+
+ Assert(rte->rtekind == RTE_SUBQUERY);
+ Assert(rte->subquery->querySource == QSRC_ROW_LEVEL_SECURITY);
+
+ if (list_length(rte->subquery->rtable) == 1)
+ var->varattno = lookup_pseudo_column(rte, var->varattno);
+ else
+ var->varattno = lookup_pseudo_column_multi(rte, var->varattno);
+
+ return false;
+ }
+ else if (IsA(node, Query))
+ {
+ bool result;
+
+ context->varlevelsup++;
+ result = query_tree_walker((Query *) node,
+ fixup_varattnos_walker,
+ (void *) context, 0);
+ context->varlevelsup--;
+
+ return result;
+ }
+ return expression_tree_walker(node,
+ fixup_varattnos_walker,
+ (void *) context);
+}
+
+/*
+ * expand_range_table
+ *
+ * It replaces the given table reference by a simple sub-query.
+ */
+static void
+expand_range_table(RangeTblEntry *rte, RowMarkClause *rclause,
+ Node *qual, int flags)
+{
+ Query *subqry;
+
+ subqry = make_pseudo_subquery(rte, rte->relid, qual, flags, rclause);
+
+ /* no permission checks are needed to subquery itself */
+ rte->requiredPerms = 0;
+ rte->checkAsUser = InvalidOid;
+ rte->selectedCols = NULL;
+ rte->modifiedCols = NULL;
+
+ rte->eref = makeAlias(get_rel_name(rte->relid), NIL);
+ rte->alias = makeAlias(get_rel_name(rte->relid), NIL);
+
+ rte->relid = InvalidOid;
+ rte->rtekind = RTE_SUBQUERY;
+ rte->subquery = subqry;
+ rte->security_barrier = (!qual ? false : true);
+ rte->inh = false;
+}
+
+/*
+ * expand_range_table_multi
+ *
+ * It replaces the given table references by an alternative sub-query that
+ * also contains multiple simple sub-queries.
+ */
+static void
+expand_range_table_multi(RangeTblEntry *rte, RowMarkClause *rclause,
+ List *rls_relids, List *rls_quals, List *rls_flags)
+{
+ Query *baseqry;
+ SetOperationStmt *setop = NULL;
+ RangeTblEntry *subrte;
+ RangeTblRef *subrtr;
+ ListCell *cell1;
+ ListCell *cell2;
+ ListCell *cell3;
+
+ Assert(rte->rtekind == RTE_RELATION && OidIsValid(rte->relid));
+ Assert(rte->relid == linitial_oid(rls_relids));
+ Assert(list_length(rls_relids) == list_length(rls_quals));
+ Assert(list_length(rls_relids) == list_length(rls_flags));
+ Assert(list_length(rls_relids) > 1);
+
+ /*
+ * baseqry is the sub-query that should replaces reference to
+ * the target relation, and will also have multiple sub-queries
+ * to scan the relations with row-level security policy.
+ */
+ baseqry = makeNode(Query);
+ baseqry->commandType = CMD_SELECT;
+ baseqry->querySource = QSRC_ROW_LEVEL_SECURITY;
+
+ forthree (cell1, rls_relids, cell2, rls_quals, cell3, rls_flags)
+ {
+ Oid relid = lfirst_oid(cell1);
+ Node *qual = lfirst(cell2);
+ int flags = lfirst_int(cell3);
+
+ /*
+ * Construct a pseudo sub-query corresponding to the table
+ * to be applied, then chain it with base-query.
+ * If no security policy is configured, here is no reason
+ * whey the sub-query should be security-barrier.
+ */
+ subrte = makeNode(RangeTblEntry);
+ subrte->rtekind = RTE_SUBQUERY;
+ subrte->subquery
+ = make_pseudo_subquery(rte, relid, qual, flags, rclause);
+ subrte->security_barrier = (!qual ? false : true);
+ subrte->alias = makeAlias(get_rel_name(relid), NIL);
+ subrte->eref = makeAlias(get_rel_name(relid), NIL);
+
+ baseqry->rtable = lappend(baseqry->rtable, subrte);
+
+ /*
+ * Also, we need to set up SetOperationStmt to connect pseudo
+ * sub-queries according to simple UNION ALL.
+ */
+ subrtr = makeNode(RangeTblRef);
+ subrtr->rtindex = list_length(baseqry->rtable);
+ if (!setop)
+ {
+ setop = makeNode(SetOperationStmt);
+ setop->op = SETOP_UNION;
+ setop->all = true;
+ setop->larg = (Node *)subrtr;
+ setop->rarg = NULL;
+ }
+ else if (!setop->rarg)
+ {
+ Assert(setop->larg != NULL);
+
+ setop->rarg = (Node *)subrtr;
+ }
+ else
+ {
+ SetOperationStmt *setop_next;
+
+ Assert(setop->larg != NULL && setop->rarg != NULL);
+
+ setop_next = makeNode(SetOperationStmt);
+ setop_next->op = SETOP_UNION;
+ setop_next->all = true;
+ setop_next->larg = (Node *) setop;
+ setop_next->rarg = (Node *) subrtr;
+
+ setop = setop_next;
+ }
+ }
+ baseqry->setOperations = (Node *) setop;
+
+ /* no permission checks are needed to subquery itself */
+ rte->requiredPerms = 0;
+ rte->checkAsUser = InvalidOid;
+ rte->selectedCols = NULL;
+ rte->modifiedCols = NULL;
+ rte->eref = makeAlias(get_rel_name(rte->relid), NIL);
+ rte->alias = makeAlias(get_rel_name(rte->relid), NIL);
+
+ rte->relid = InvalidOid;
+ rte->rtekind = RTE_SUBQUERY;
+ rte->subquery = baseqry;
+ rte->security_barrier = false;
+}
+
+/*
+ * apply_relation_rowlevelsec
+ *
+ * This routine pulls row-level security policy of the target relaion
+ * given as range table entry.
+ */
+static bool
+apply_relation_rowlevelsec(Query *parse, RangeTblEntry *rte, Index rtindex,
+ List *active_relids)
+{
+ Relation rel;
+ LOCKMODE lockmode;
+ List *relid_list = NIL;
+ List *quals_list = NIL;
+ List *flags_list = NIL;
+ ListCell *cell;
+ bool result = false;
+
+ Assert(rte->rtekind == RTE_RELATION && OidIsValid(rte->relid));
+
+ /*
+ * XXX - Right now, we don't care about result relation of
+ * UPDATE or DELETE statement. So, ignore it simply.
+ */
+ if (parse->resultRelation == rtindex)
+ return false;
+
+ if (!rte->inh)
+ {
+ lockmode = NoLock;
+ relid_list = list_make1_oid(rte->relid);
+ }
+ else
+ {
+ /*
+ * In case when the target relation may have inheritances,
+ * we need to suggest an appropriate lock mode because it
+ * is the first time to reference these tables in a series
+ * of processes. For more details, see the comments in
+ * expand_inherited_tables.
+ * Also note that it does not guarantee the locks on child
+ * tables being already acquired at expand_inherited_tables,
+ * because row-level security routines can be bypassed if
+ * RowLevelSecModeDisabled.
+ */
+ if (rtindex == parse->resultRelation)
+ lockmode = RowExclusiveLock;
+ else
+ {
+ foreach (cell, parse->rowMarks)
+ {
+ Assert(IsA(lfirst(cell), RowMarkClause));
+ if (((RowMarkClause *) lfirst(cell))->rti == rtindex)
+ break;
+ }
+ if (cell)
+ lockmode = RowShareLock;
+ else
+ lockmode = AccessShareLock;
+ }
+ relid_list = find_all_inheritors(rte->relid, lockmode, NULL);
+ }
+
+ /*
+ * Try to fetch row-level security policy of the target relation
+ * or its children.
+ */
+ foreach (cell, relid_list)
+ {
+ Expr *qual = NULL;
+ int flags = 0;
+
+ rel = heap_open(lfirst_oid(cell), lockmode);
+
+ /*
+ * Recursion check to prevent infinite recursion.
+ */
+ if (list_member_oid(active_relids, RelationGetRelid(rel)))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("infinite recursion detected for relation \"%s\"",
+ RelationGetRelationName(rel))));
+
+ /*
+ * Pull out row-level security policy configured with built-in
+ * features, if unprivileged users. Please note that superuser
+ * can bypass it.
+ */
+ if (rel->rlsdesc && !superuser())
+ {
+ RowLevelSecDesc *rlsdesc = rel->rlsdesc;
+
+ qual = 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)(rel);
+ if (temp != NIL)
+ {
+ if ((flags & RLS_FLAG_HAS_SUBLINKS) == 0 &&
+ contain_subplans((Node *) temp))
+ flags |= RLS_FLAG_HAS_SUBLINKS;
+
+ if (qual != NULL)
+ temp = lappend(temp, qual);
+
+ if (list_length(temp) == 1)
+ qual = (Expr *)list_head(temp);
+ else
+ qual = makeBoolExpr(AND_EXPR, temp, -1);
+ }
+ }
+
+ /*
+ * If either built-in or extension row-level security policy
+ * is configured at least, we determine this table reference
+ * should be replaced with an alternative sub-query.
+ */
+ if (qual)
+ {
+ /* SubLink should be also extracted */
+ if (flags & RLS_FLAG_HAS_SUBLINKS)
+ {
+ fixup_sublink_context context;
+
+ memset(&context, 0, sizeof(fixup_sublink_context));
+
+ context.exec_query_rewriter = true;
+ context.active_relids = lappend_oid(list_copy(active_relids),
+ RelationGetRelid(rel));
+ fixup_sublink_walker((Node *)qual, (void *) &context);
+
+ list_free(context.active_relids);
+ }
+ result = true;
+ }
+ quals_list = lappend(quals_list, qual);
+ flags_list = lappend_int(flags_list, flags);
+
+ heap_close(rel, NoLock); /* close the relation, but keep locks */
+ }
+
+ if (result)
+ {
+ RowMarkClause *rclause = NULL;
+
+ /*
+ * Push-down row-level lock of the target relation, since sub-query
+ * does not support FOR SHARE/FOR UPDATE locks being assigned.
+ */
+ foreach (cell, parse->rowMarks)
+ {
+ if (((RowMarkClause *) lfirst(cell))->rti == rtindex)
+ {
+ rclause = lfirst(cell);
+ parse->rowMarks = list_delete(parse->rowMarks, rclause);
+ break;
+ }
+ }
+
+ /*
+ * If no children, a range table entry of relation is replaced
+ * with a simple sub-query. On the other hand, it is replaced
+ * with a sub-query that also contains simple but multiple sub-
+ * queries being connected with UNION ALL.
+ */
+ if (list_length(relid_list) == 1)
+ expand_range_table(rte, rclause,
+ linitial(quals_list),
+ linitial_int(flags_list));
+ else
+ expand_range_table_multi(rte, rclause,
+ relid_list, quals_list, flags_list);
+ }
+ list_free(relid_list);
+ list_free(quals_list);
+ list_free(flags_list);
+
+ return result;
+}
+
+/*
+ * apply_subquery_rowlevelsec
+ *
+ * This routine applies row-level security policy of the table being
+ * referenced in the given sub- or toplevel- query recursively.
+ */
+static bool
+apply_subquery_rowlevelsec(Query *parse, List *active_relids)
+{
+ Index rtindex = 0;
+ bool has_rowlevel_sec = false;
+ ListCell *cell;
+
+ foreach (cell, parse->rtable)
+ {
+ RangeTblEntry *rte = lfirst(cell);
+ rtindex++;
+
+ if (rte->rtekind == RTE_RELATION)
+ {
+ if (apply_relation_rowlevelsec(parse, rte, rtindex,
+ active_relids))
+ {
+ /*
+ * If the referenced relation was replaced with
+ * a sub-query, varattno of Var node needs to be
+ * adjusted according to the target-list of the
+ * pseudo sub-query.
+ */
+ fixup_varattnos_context context;
+
+ context.parse = parse;
+ context.varno = rtindex;
+ context.varlevelsup = 0;
+
+ query_tree_walker(parse, fixup_varattnos_walker,
+ (void *) &context, 0);
+
+ has_rowlevel_sec = true;
+ }
+ }
+ else if (rte->rtekind == RTE_SUBQUERY)
+ {
+ if (apply_subquery_rowlevelsec(rte->subquery, active_relids))
+ has_rowlevel_sec = true;
+ }
+ }
+
+ /*
+ * If the given parse tree contains SubLink node, we need to walk
+ * into the parse tree to extract them in addition to the range-
+ * table entries.
+ * Query->hasSubLinks gives a hint for optimization to skip it.
+ */
+ if (parse->hasSubLinks)
+ {
+ fixup_sublink_context context;
+
+ memset(&context, 0, sizeof(fixup_sublink_context));
+ context.active_relids = active_relids;
+
+ query_tree_walker(parse, fixup_sublink_walker,
+ (void *) &context,
+ QTW_IGNORE_RANGE_TABLE);
+ if (context.has_rowlevel_sec)
+ has_rowlevel_sec = true;
+ }
+ return has_rowlevel_sec;
+}
+
+/*
+ * applyRowLevelSecurity
+ *
+ * It applies row-level security policy of the relation, if a table has
+ * its security policy, reference to the table shall be replaced by sub-
+ * query that simply scan the table being replaced with security policy
+ * as qualifiers. Of course, security-barrier flag is also added to the
+ * sub-query to prevent unintentional push-down of malicious functions
+ * being executed prior to evaluation of security policy.
+ * In the result, all user can see is rows of the relation that satisfies
+ * the qualifier supplied as security policy.
+ *
+ * One complex stuff is table reference with inherited child tables.
+ * In case when the give parse tree referenced a table with child tables,
+ * and one of them has row-level security, reference to the parent table
+ * also shall be replaced by a sub-query that contains multiple queries
+ * connected with UNION ALL.
+ *
+ * For example, if table t1 has t2 and t3 as children, security policy
+ * of t1 is (x > 10) and t3 is (x % 2 = 0), the following query
+ * SELECT * FROM t1 WHERE f_leak(y)
+ * should be rewritten as if
+ * SELECT * FROM (
+ * SELECT x, y FROM only t1 WHERE x > 10
+ * UNION ALL
+ * SELECT x, y FROM only t2
+ * UNION ALL
+ * SELECT x, y FROM only t3 WHERE x % 2 = 0
+ * ) t1 WHERE f_leak(y);
+ * would be given. It is identical with how table inheritance performs on.
+ *
+ * Row-level security policy can contain SubLink, thus, it allows to refer
+ * another relation to make access control decision. Even if it contains
+ * view, it shall be extracted to the normal form. In addition, row-level
+ * security policy of underlying tables are also applied recursively.
+ */
+void
+applyRowLevelSecurity(PlannerGlobal *glob, Query *parse)
+{
+ /* mode checks */
+ if (rowlevel_security_mode == RowLevelSecModeDisabled)
+ return;
+
+ /*
+ * Extract row-level security policy of the underlying tables
+ * recursively, and replace the references by sub-query with
+ * security-barrier and qualifiers to filter out rows.
+ * Plan has to be invalidated if different user-id was applied
+ * on executor stage.
+ */
+ if (apply_subquery_rowlevelsec(parse, NIL))
+ {
+ PlanInvalItem *pi_item;
+
+ glob->planUserId = GetUserId();
+
+ pi_item = makeNode(PlanInvalItem);
+ pi_item->cacheId = AUTHOID;
+ pi_item->hashValue =
+ GetSysCacheHashValue1(AUTHOID,
+ ObjectIdGetDatum(glob->planUserId));
+ glob->invalItems = lappend(glob->invalItems, pi_item);
+ }
+}
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 8f75948..f08fcd8 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -2245,3 +2245,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..19a2255 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -43,6 +43,7 @@
#include "parser/parse_coerce.h"
#include "parser/parse_relation.h"
#include "miscadmin.h"
+#include "optimizer/rowlevelsec.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
#include "utils/guc.h"
@@ -2999,6 +3000,7 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
int spi_result;
Oid save_userid;
int save_sec_context;
+ RowLevelSecMode save_rls_mode;
Datum vals[RI_MAX_NUMKEYS * 2];
char nulls[RI_MAX_NUMKEYS * 2];
@@ -3081,6 +3083,15 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
SetUserIdAndSecContext(RelationGetForm(query_rel)->relowner,
save_sec_context | SECURITY_LOCAL_USERID_CHANGE);
+ /*
+ * Disabled row-level security in case when foreign-key relation is
+ * queried to check existence of tupls that references the tuple to
+ * be modified on the primary-key side.
+ */
+ save_rls_mode = getRowLevelSecurityMode();
+ if (source_is_pk)
+ setRowLevelSecurityMode(RowLevelSecModeDisabled);
+
/* Finally we can run the query. */
spi_result = SPI_execute_snapshot(qplan,
vals, nulls,
@@ -3090,6 +3101,9 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
/* Restore UID and security context */
SetUserIdAndSecContext(save_userid, save_sec_context);
+ /* Restore row-level security performing mode */
+ setRowLevelSecurityMode(save_rls_mode);
+
/* Check result */
if (spi_result < 0)
elog(ERROR, "SPI_execute_snapshot returned %d", spi_result);
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 dd2019a..e18d2f4 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -3867,6 +3867,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");
@@ -3891,7 +3892,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
@@ -3911,7 +3950,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 "
@@ -3947,7 +3987,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 "
@@ -3982,7 +4023,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 "
@@ -4017,7 +4059,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 "
@@ -4053,7 +4096,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 "
@@ -4088,7 +4132,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 "
@@ -4119,7 +4164,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",
@@ -4145,7 +4191,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",
@@ -4181,7 +4228,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",
@@ -4229,6 +4277,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)
{
@@ -4271,6 +4320,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))
{
@@ -12803,6 +12856,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 2aa2060..5485101 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -256,6 +256,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..5a64d1b
--- /dev/null
+++ b/src/include/catalog/pg_rowlevelsec.h
@@ -0,0 +1,60 @@
+/*
+ * 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 ResetRowLevelSecurity(Relation relation);
+extern void RemoveRowLevelSecurityById(Oid relationId);
+
+#endif /* PG_ROWLEVELSEC_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 09b15e7..4118794 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 */
@@ -1232,6 +1233,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 2b2742d..f20923a 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..a09e15c
--- /dev/null
+++ b/src/include/optimizer/rowlevelsec.h
@@ -0,0 +1,31 @@
+/* -------------------------------------------------------------------------
+ *
+ * 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;
+
+typedef enum {
+ RowLevelSecModeEnabled,
+ RowLevelSecModeDisabled,
+} RowLevelSecMode;
+
+extern RowLevelSecMode getRowLevelSecurityMode(void);
+extern void setRowLevelSecurityMode(RowLevelSecMode new_mode);
+
+extern void applyRowLevelSecurity(PlannerGlobal *root, Query *parse);
+
+#endif /* ROWLEVELSEC_H */
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index e3bb35f..d13ef32 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..46921cf
--- /dev/null
+++ b/src/test/regress/expected/rowlevelsec.out
@@ -0,0 +1,853 @@
+--
+-- 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;
+CREATE TABLE account (
+ pguser name primary key,
+ slevel int,
+ scategory bit(4)
+);
+GRANT SELECT,REFERENCES ON TABLE account TO public;
+INSERT INTO account VALUES
+ ('rls_regress_user1', 1, B'0011'),
+ ('rls_regress_user2', 2, B'0110'),
+ ('rls_regress_user3', 0, B'0101');
+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;
+-- Creation of Test Data
+SET SESSION AUTHORIZATION rls_regress_user0;
+CREATE TABLE document (
+ did int primary key,
+ dlevel int,
+ dcategory bit(4),
+ dtitle text
+);
+GRANT ALL ON document TO public;
+INSERT INTO document VALUES
+ ( 10, 0, B'0000', 'this document is unclassified, category(----)'),
+ ( 20, 0, B'0001', 'this document is unclassified, category(---A)'),
+ ( 30, 0, B'0010', 'this document is unclassified, category(--B-)'),
+ ( 40, 0, B'0011', 'this document is unclassified, category(--BA)'),
+ ( 50, 0, B'0100', 'this document is unclassified, category(-C--)'),
+ ( 60, 0, B'0101', 'this document is unclassified, category(-C-A)'),
+ ( 70, 0, B'0110', 'this document is unclassified, category(-CB-)'),
+ ( 80, 0, B'0111', 'this document is unclassified, category(-CBA)'),
+ ( 90, 1, B'0000', 'this document is classified, category(----)'),
+ (100, 1, B'0001', 'this document is classified, category(---A)'),
+ (110, 1, B'0010', 'this document is classified, category(--B-)'),
+ (120, 1, B'0011', 'this document is classified, category(--BA)'),
+ (130, 1, B'0100', 'this document is classified, category(-C--)'),
+ (140, 1, B'0101', 'this document is classified, category(-C-A)'),
+ (150, 1, B'0110', 'this document is classified, category(-CB-)'),
+ (160, 1, B'0111', 'this document is classified, category(-CBA)'),
+ (170, 2, B'0000', 'this document is secret, category(----)'),
+ (180, 2, B'0001', 'this document is secret, category(---A)'),
+ (190, 2, B'0010', 'this document is secret, category(--B-)'),
+ (200, 2, B'0011', 'this document is secret, category(--BA)'),
+ (210, 2, B'0100', 'this document is secret, category(-C--)'),
+ (220, 2, B'0101', 'this document is secret, category(-C-A)'),
+ (230, 2, B'0110', 'this document is secret, category(-CB-)'),
+ (240, 2, B'0111', 'this document is secret, category(-CBA)');
+CREATE TABLE browse (
+ pguser name references account(pguser),
+ did int references document(did),
+ ymd date
+);
+GRANT ALL ON browse TO public;
+INSERT INTO browse VALUES
+ ('rls_regress_user1', 20, '2012-07-01'),
+ ('rls_regress_user1', 40, '2012-07-02'),
+ ('rls_regress_user1', 110, '2012-07-03'),
+ ('rls_regress_user2', 30, '2012-07-04'),
+ ('rls_regress_user2', 50, '2012-07-05'),
+ ('rls_regress_user2', 90, '2012-07-06'),
+ ('rls_regress_user2', 130, '2012-07-07'),
+ ('rls_regress_user2', 150, '2012-07-08'),
+ ('rls_regress_user2', 150, '2012-07-08'),
+ ('rls_regress_user2', 190, '2012-07-09'),
+ ('rls_regress_user2', 210, '2012-07-10'),
+ ('rls_regress_user3', 10, '2012-07-11'),
+ ('rls_regress_user3', 50, '2012-07-12');
+-- user's sensitivity level must higher than document's level
+ALTER TABLE document SET ROW LEVEL SECURITY
+ (dlevel <= (SELECT slevel FROM account WHERE pguser = current_user));
+ALTER TABLE browse SET ROW LEVEL SECURITY
+ (pguser = current_user);
+-- viewpoint from rls_regress_user1
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE: f_leak => this document is unclassified, category(----)
+NOTICE: f_leak => this document is unclassified, category(---A)
+NOTICE: f_leak => this document is unclassified, category(--B-)
+NOTICE: f_leak => this document is unclassified, category(--BA)
+NOTICE: f_leak => this document is unclassified, category(-C--)
+NOTICE: f_leak => this document is unclassified, category(-C-A)
+NOTICE: f_leak => this document is unclassified, category(-CB-)
+NOTICE: f_leak => this document is unclassified, category(-CBA)
+NOTICE: f_leak => this document is classified, category(----)
+NOTICE: f_leak => this document is classified, category(---A)
+NOTICE: f_leak => this document is classified, category(--B-)
+NOTICE: f_leak => this document is classified, category(--BA)
+NOTICE: f_leak => this document is classified, category(-C--)
+NOTICE: f_leak => this document is classified, category(-C-A)
+NOTICE: f_leak => this document is classified, category(-CB-)
+NOTICE: f_leak => this document is classified, category(-CBA)
+ did | dlevel | dcategory | dtitle
+-----+--------+-----------+-----------------------------------------------
+ 10 | 0 | 0000 | this document is unclassified, category(----)
+ 20 | 0 | 0001 | this document is unclassified, category(---A)
+ 30 | 0 | 0010 | this document is unclassified, category(--B-)
+ 40 | 0 | 0011 | this document is unclassified, category(--BA)
+ 50 | 0 | 0100 | this document is unclassified, category(-C--)
+ 60 | 0 | 0101 | this document is unclassified, category(-C-A)
+ 70 | 0 | 0110 | this document is unclassified, category(-CB-)
+ 80 | 0 | 0111 | this document is unclassified, category(-CBA)
+ 90 | 1 | 0000 | this document is classified, category(----)
+ 100 | 1 | 0001 | this document is classified, category(---A)
+ 110 | 1 | 0010 | this document is classified, category(--B-)
+ 120 | 1 | 0011 | this document is classified, category(--BA)
+ 130 | 1 | 0100 | this document is classified, category(-C--)
+ 140 | 1 | 0101 | this document is classified, category(-C-A)
+ 150 | 1 | 0110 | this document is classified, category(-CB-)
+ 160 | 1 | 0111 | this document is classified, category(-CBA)
+(16 rows)
+
+SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+NOTICE: f_leak => (rls_regress_user1,20,07-01-2012)
+NOTICE: f_leak => (rls_regress_user1,40,07-02-2012)
+NOTICE: f_leak => (rls_regress_user1,110,07-03-2012)
+ did | dlevel | dcategory | dtitle | pguser | ymd
+-----+--------+-----------+-----------------------------------------------+-------------------+------------
+ 20 | 0 | 0001 | this document is unclassified, category(---A) | rls_regress_user1 | 07-01-2012
+ 40 | 0 | 0011 | this document is unclassified, category(--BA) | rls_regress_user1 | 07-02-2012
+ 110 | 1 | 0010 | this document is classified, category(--B-) | rls_regress_user1 | 07-03-2012
+(3 rows)
+
+-- viewpoint from rls_regress_user2
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE: f_leak => this document is unclassified, category(----)
+NOTICE: f_leak => this document is unclassified, category(---A)
+NOTICE: f_leak => this document is unclassified, category(--B-)
+NOTICE: f_leak => this document is unclassified, category(--BA)
+NOTICE: f_leak => this document is unclassified, category(-C--)
+NOTICE: f_leak => this document is unclassified, category(-C-A)
+NOTICE: f_leak => this document is unclassified, category(-CB-)
+NOTICE: f_leak => this document is unclassified, category(-CBA)
+NOTICE: f_leak => this document is classified, category(----)
+NOTICE: f_leak => this document is classified, category(---A)
+NOTICE: f_leak => this document is classified, category(--B-)
+NOTICE: f_leak => this document is classified, category(--BA)
+NOTICE: f_leak => this document is classified, category(-C--)
+NOTICE: f_leak => this document is classified, category(-C-A)
+NOTICE: f_leak => this document is classified, category(-CB-)
+NOTICE: f_leak => this document is classified, category(-CBA)
+NOTICE: f_leak => this document is secret, category(----)
+NOTICE: f_leak => this document is secret, category(---A)
+NOTICE: f_leak => this document is secret, category(--B-)
+NOTICE: f_leak => this document is secret, category(--BA)
+NOTICE: f_leak => this document is secret, category(-C--)
+NOTICE: f_leak => this document is secret, category(-C-A)
+NOTICE: f_leak => this document is secret, category(-CB-)
+NOTICE: f_leak => this document is secret, category(-CBA)
+ did | dlevel | dcategory | dtitle
+-----+--------+-----------+-----------------------------------------------
+ 10 | 0 | 0000 | this document is unclassified, category(----)
+ 20 | 0 | 0001 | this document is unclassified, category(---A)
+ 30 | 0 | 0010 | this document is unclassified, category(--B-)
+ 40 | 0 | 0011 | this document is unclassified, category(--BA)
+ 50 | 0 | 0100 | this document is unclassified, category(-C--)
+ 60 | 0 | 0101 | this document is unclassified, category(-C-A)
+ 70 | 0 | 0110 | this document is unclassified, category(-CB-)
+ 80 | 0 | 0111 | this document is unclassified, category(-CBA)
+ 90 | 1 | 0000 | this document is classified, category(----)
+ 100 | 1 | 0001 | this document is classified, category(---A)
+ 110 | 1 | 0010 | this document is classified, category(--B-)
+ 120 | 1 | 0011 | this document is classified, category(--BA)
+ 130 | 1 | 0100 | this document is classified, category(-C--)
+ 140 | 1 | 0101 | this document is classified, category(-C-A)
+ 150 | 1 | 0110 | this document is classified, category(-CB-)
+ 160 | 1 | 0111 | this document is classified, category(-CBA)
+ 170 | 2 | 0000 | this document is secret, category(----)
+ 180 | 2 | 0001 | this document is secret, category(---A)
+ 190 | 2 | 0010 | this document is secret, category(--B-)
+ 200 | 2 | 0011 | this document is secret, category(--BA)
+ 210 | 2 | 0100 | this document is secret, category(-C--)
+ 220 | 2 | 0101 | this document is secret, category(-C-A)
+ 230 | 2 | 0110 | this document is secret, category(-CB-)
+ 240 | 2 | 0111 | this document is secret, category(-CBA)
+(24 rows)
+
+SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+NOTICE: f_leak => (rls_regress_user2,30,07-04-2012)
+NOTICE: f_leak => (rls_regress_user2,50,07-05-2012)
+NOTICE: f_leak => (rls_regress_user2,90,07-06-2012)
+NOTICE: f_leak => (rls_regress_user2,130,07-07-2012)
+NOTICE: f_leak => (rls_regress_user2,150,07-08-2012)
+NOTICE: f_leak => (rls_regress_user2,150,07-08-2012)
+NOTICE: f_leak => (rls_regress_user2,190,07-09-2012)
+NOTICE: f_leak => (rls_regress_user2,210,07-10-2012)
+ did | dlevel | dcategory | dtitle | pguser | ymd
+-----+--------+-----------+-----------------------------------------------+-------------------+------------
+ 30 | 0 | 0010 | this document is unclassified, category(--B-) | rls_regress_user2 | 07-04-2012
+ 50 | 0 | 0100 | this document is unclassified, category(-C--) | rls_regress_user2 | 07-05-2012
+ 90 | 1 | 0000 | this document is classified, category(----) | rls_regress_user2 | 07-06-2012
+ 130 | 1 | 0100 | this document is classified, category(-C--) | rls_regress_user2 | 07-07-2012
+ 150 | 1 | 0110 | this document is classified, category(-CB-) | rls_regress_user2 | 07-08-2012
+ 150 | 1 | 0110 | this document is classified, category(-CB-) | rls_regress_user2 | 07-08-2012
+ 190 | 2 | 0010 | this document is secret, category(--B-) | rls_regress_user2 | 07-09-2012
+ 210 | 2 | 0100 | this document is secret, category(-C--) | rls_regress_user2 | 07-10-2012
+(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 account_pkey on account
+ Index Cond: (pguser = "current_user"())
+(7 rows)
+
+EXPLAIN SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+ QUERY PLAN
+------------------------------------------------------------------------------------------
+ Hash Join (cost=30.34..57.95 rows=2 width=117)
+ Hash Cond: (document.did = browse.did)
+ -> Seq Scan on document (cost=8.27..31.15 rows=343 width=49)
+ Filter: (dlevel <= $0)
+ InitPlan 1 (returns $0)
+ -> Index Scan using account_pkey on account (cost=0.00..8.27 rows=1 width=4)
+ Index Cond: (pguser = "current_user"())
+ -> Hash (cost=22.06..22.06 rows=1 width=72)
+ -> Subquery Scan on browse (cost=0.00..22.06 rows=1 width=72)
+ Filter: f_leak((browse.browse)::text)
+ -> Seq Scan on browse browse_1 (cost=0.00..22.00 rows=4 width=168)
+ Filter: (pguser = "current_user"())
+(12 rows)
+
+-- change row-level security policy
+ALTER TABLE document RESET ROW LEVEL SECURITY; -- failed
+ERROR: must be owner of relation document
+ALTER TABLE document SET ROW LEVEL SECURITY (true); -- failed
+ERROR: must be owner of relation document
+SET SESSION AUTHORIZATION rls_regress_user0;
+-- switch to category based control from level based
+ALTER TABLE document SET ROW LEVEL SECURITY
+ (dcategory & (SELECT ~scategory FROM account WHERE pguser = current_user) = B'0000');
+-- viewpoint from rls_regress_user1 again
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE: f_leak => this document is unclassified, category(----)
+NOTICE: f_leak => this document is unclassified, category(---A)
+NOTICE: f_leak => this document is unclassified, category(--B-)
+NOTICE: f_leak => this document is unclassified, category(--BA)
+NOTICE: f_leak => this document is classified, category(----)
+NOTICE: f_leak => this document is classified, category(---A)
+NOTICE: f_leak => this document is classified, category(--B-)
+NOTICE: f_leak => this document is classified, category(--BA)
+NOTICE: f_leak => this document is secret, category(----)
+NOTICE: f_leak => this document is secret, category(---A)
+NOTICE: f_leak => this document is secret, category(--B-)
+NOTICE: f_leak => this document is secret, category(--BA)
+ did | dlevel | dcategory | dtitle
+-----+--------+-----------+-----------------------------------------------
+ 10 | 0 | 0000 | this document is unclassified, category(----)
+ 20 | 0 | 0001 | this document is unclassified, category(---A)
+ 30 | 0 | 0010 | this document is unclassified, category(--B-)
+ 40 | 0 | 0011 | this document is unclassified, category(--BA)
+ 90 | 1 | 0000 | this document is classified, category(----)
+ 100 | 1 | 0001 | this document is classified, category(---A)
+ 110 | 1 | 0010 | this document is classified, category(--B-)
+ 120 | 1 | 0011 | this document is classified, category(--BA)
+ 170 | 2 | 0000 | this document is secret, category(----)
+ 180 | 2 | 0001 | this document is secret, category(---A)
+ 190 | 2 | 0010 | this document is secret, category(--B-)
+ 200 | 2 | 0011 | this document is secret, category(--BA)
+(12 rows)
+
+SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+NOTICE: f_leak => (rls_regress_user1,20,07-01-2012)
+NOTICE: f_leak => (rls_regress_user1,40,07-02-2012)
+NOTICE: f_leak => (rls_regress_user1,110,07-03-2012)
+ did | dlevel | dcategory | dtitle | pguser | ymd
+-----+--------+-----------+-----------------------------------------------+-------------------+------------
+ 20 | 0 | 0001 | this document is unclassified, category(---A) | rls_regress_user1 | 07-01-2012
+ 40 | 0 | 0011 | this document is unclassified, category(--BA) | rls_regress_user1 | 07-02-2012
+ 110 | 1 | 0010 | this document is classified, category(--B-) | rls_regress_user1 | 07-03-2012
+(3 rows)
+
+-- viewpoint from rls_regress_user2 again
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE: f_leak => this document is unclassified, category(----)
+NOTICE: f_leak => this document is unclassified, category(--B-)
+NOTICE: f_leak => this document is unclassified, category(-C--)
+NOTICE: f_leak => this document is unclassified, category(-CB-)
+NOTICE: f_leak => this document is classified, category(----)
+NOTICE: f_leak => this document is classified, category(--B-)
+NOTICE: f_leak => this document is classified, category(-C--)
+NOTICE: f_leak => this document is classified, category(-CB-)
+NOTICE: f_leak => this document is secret, category(----)
+NOTICE: f_leak => this document is secret, category(--B-)
+NOTICE: f_leak => this document is secret, category(-C--)
+NOTICE: f_leak => this document is secret, category(-CB-)
+ did | dlevel | dcategory | dtitle
+-----+--------+-----------+-----------------------------------------------
+ 10 | 0 | 0000 | this document is unclassified, category(----)
+ 30 | 0 | 0010 | this document is unclassified, category(--B-)
+ 50 | 0 | 0100 | this document is unclassified, category(-C--)
+ 70 | 0 | 0110 | this document is unclassified, category(-CB-)
+ 90 | 1 | 0000 | this document is classified, category(----)
+ 110 | 1 | 0010 | this document is classified, category(--B-)
+ 130 | 1 | 0100 | this document is classified, category(-C--)
+ 150 | 1 | 0110 | this document is classified, category(-CB-)
+ 170 | 2 | 0000 | this document is secret, category(----)
+ 190 | 2 | 0010 | this document is secret, category(--B-)
+ 210 | 2 | 0100 | this document is secret, category(-C--)
+ 230 | 2 | 0110 | this document is secret, category(-CB-)
+(12 rows)
+
+SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+NOTICE: f_leak => (rls_regress_user2,30,07-04-2012)
+NOTICE: f_leak => (rls_regress_user2,50,07-05-2012)
+NOTICE: f_leak => (rls_regress_user2,90,07-06-2012)
+NOTICE: f_leak => (rls_regress_user2,130,07-07-2012)
+NOTICE: f_leak => (rls_regress_user2,150,07-08-2012)
+NOTICE: f_leak => (rls_regress_user2,150,07-08-2012)
+NOTICE: f_leak => (rls_regress_user2,190,07-09-2012)
+NOTICE: f_leak => (rls_regress_user2,210,07-10-2012)
+ did | dlevel | dcategory | dtitle | pguser | ymd
+-----+--------+-----------+-----------------------------------------------+-------------------+------------
+ 30 | 0 | 0010 | this document is unclassified, category(--B-) | rls_regress_user2 | 07-04-2012
+ 50 | 0 | 0100 | this document is unclassified, category(-C--) | rls_regress_user2 | 07-05-2012
+ 90 | 1 | 0000 | this document is classified, category(----) | rls_regress_user2 | 07-06-2012
+ 130 | 1 | 0100 | this document is classified, category(-C--) | rls_regress_user2 | 07-07-2012
+ 150 | 1 | 0110 | this document is classified, category(-CB-) | rls_regress_user2 | 07-08-2012
+ 150 | 1 | 0110 | this document is classified, category(-CB-) | rls_regress_user2 | 07-08-2012
+ 190 | 2 | 0010 | this document is secret, category(--B-) | rls_regress_user2 | 07-09-2012
+ 210 | 2 | 0100 | this document is secret, category(-C--) | rls_regress_user2 | 07-10-2012
+(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: ((dcategory & $0) = B'0000'::"bit")
+ InitPlan 1 (returns $0)
+ -> Index Scan using account_pkey on account
+ Index Cond: (pguser = "current_user"())
+(7 rows)
+
+EXPLAIN SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+ QUERY PLAN
+------------------------------------------------------------------------------------------
+ Nested Loop (cost=8.27..55.90 rows=1 width=117)
+ Join Filter: (document.did = browse.did)
+ -> Subquery Scan on browse (cost=0.00..22.06 rows=1 width=72)
+ Filter: f_leak((browse.browse)::text)
+ -> Seq Scan on browse browse_1 (cost=0.00..22.00 rows=4 width=168)
+ Filter: (pguser = "current_user"())
+ -> Seq Scan on document (cost=8.27..33.72 rows=5 width=49)
+ Filter: ((dcategory & $0) = B'0000'::"bit")
+ InitPlan 1 (returns $0)
+ -> Index Scan using account_pkey on account (cost=0.00..8.27 rows=1 width=9)
+ Index Cond: (pguser = "current_user"())
+(11 rows)
+
+-- Failed to update PK row referenced by invisible FK
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document NATURAL LEFT JOIN browse;
+ did | dlevel | dcategory | dtitle | pguser | ymd
+-----+--------+-----------+-----------------------------------------------+-------------------+------------
+ 10 | 0 | 0000 | this document is unclassified, category(----) | |
+ 20 | 0 | 0001 | this document is unclassified, category(---A) | rls_regress_user1 | 07-01-2012
+ 30 | 0 | 0010 | this document is unclassified, category(--B-) | |
+ 40 | 0 | 0011 | this document is unclassified, category(--BA) | rls_regress_user1 | 07-02-2012
+ 90 | 1 | 0000 | this document is classified, category(----) | |
+ 100 | 1 | 0001 | this document is classified, category(---A) | |
+ 110 | 1 | 0010 | this document is classified, category(--B-) | rls_regress_user1 | 07-03-2012
+ 120 | 1 | 0011 | this document is classified, category(--BA) | |
+ 170 | 2 | 0000 | this document is secret, category(----) | |
+ 180 | 2 | 0001 | this document is secret, category(---A) | |
+ 190 | 2 | 0010 | this document is secret, category(--B-) | |
+ 200 | 2 | 0011 | this document is secret, category(--BA) | |
+(12 rows)
+
+DELETE FROM document WHERE did = 30; -- failed
+ERROR: update or delete on table "document" violates foreign key constraint "browse_did_fkey" on table "browse"
+DETAIL: Key (did)=(30) is still referenced from table "browse".
+UPDATE document SET did = 9999 WHERE did = 90; -- failed
+ERROR: update or delete on table "document" violates foreign key constraint "browse_did_fkey" on table "browse"
+DETAIL: Key (did)=(90) is still referenced from table "browse".
+--
+-- 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
+ -> Seq Scan on t1
+ Filter: ((a % 2) = 0)
+ -> Seq Scan on t2
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+(7 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
+ -> Seq Scan on t1
+ Filter: ((a % 2) = 0)
+ -> Seq Scan on t2
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+(7 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
+ -> Seq Scan on t1
+ Filter: ((a % 2) = 0)
+ -> Seq Scan on t2
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+(7 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
+-------------------------------------------------
+ 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)
+ -> Subquery Scan on t3
+ -> LockRows
+ -> Seq Scan on t3 t3_1
+(13 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
+-------------------------------------------------
+ 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)
+ -> Subquery Scan on t3
+ -> LockRows
+ -> Seq Scan on t3 t3_1
+ Filter: f_leak(b)
+(16 rows)
+
+--
+-- 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"
+-- Now COPY TO command does not support RLS
+-- COPY t1 TO stdin;
+-- 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
+ -> Seq Scan on t1
+ Filter: ((a <= 2) AND ((a % 2) = 0))
+ -> Seq Scan on t2
+ Filter: ((a <= 2) AND ((a % 2) = 1))
+ -> Seq Scan on t3
+ Filter: (a <= 2)
+(8 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
+ -> Seq Scan on t1
+ Filter: ((a = 2) AND ((a % 2) = 0))
+ -> Seq Scan on t2
+ Filter: ((a = 2) AND ((a % 2) = 1))
+ -> Seq Scan on t3
+ Filter: (a = 2)
+(8 rows)
+
+--
+-- Clean up objects
+--
+RESET SESSION AUTHORIZATION;
+DROP SCHEMA rls_regress_schema CASCADE;
+NOTICE: drop cascades to 10 other objects
+DETAIL: drop cascades to table account
+drop cascades to function f_leak(text)
+drop cascades to table document
+drop cascades to table browse
+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..21def6e
--- /dev/null
+++ b/src/test/regress/sql/rowlevelsec.sql
@@ -0,0 +1,273 @@
+--
+-- 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;
+
+CREATE TABLE account (
+ pguser name primary key,
+ slevel int,
+ scategory bit(4)
+);
+GRANT SELECT,REFERENCES ON TABLE account TO public;
+INSERT INTO account VALUES
+ ('rls_regress_user1', 1, B'0011'),
+ ('rls_regress_user2', 2, B'0110'),
+ ('rls_regress_user3', 0, B'0101');
+
+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;
+
+-- Creation of Test Data
+SET SESSION AUTHORIZATION rls_regress_user0;
+
+CREATE TABLE document (
+ did int primary key,
+ dlevel int,
+ dcategory bit(4),
+ dtitle text
+);
+GRANT ALL ON document TO public;
+INSERT INTO document VALUES
+ ( 10, 0, B'0000', 'this document is unclassified, category(----)'),
+ ( 20, 0, B'0001', 'this document is unclassified, category(---A)'),
+ ( 30, 0, B'0010', 'this document is unclassified, category(--B-)'),
+ ( 40, 0, B'0011', 'this document is unclassified, category(--BA)'),
+ ( 50, 0, B'0100', 'this document is unclassified, category(-C--)'),
+ ( 60, 0, B'0101', 'this document is unclassified, category(-C-A)'),
+ ( 70, 0, B'0110', 'this document is unclassified, category(-CB-)'),
+ ( 80, 0, B'0111', 'this document is unclassified, category(-CBA)'),
+ ( 90, 1, B'0000', 'this document is classified, category(----)'),
+ (100, 1, B'0001', 'this document is classified, category(---A)'),
+ (110, 1, B'0010', 'this document is classified, category(--B-)'),
+ (120, 1, B'0011', 'this document is classified, category(--BA)'),
+ (130, 1, B'0100', 'this document is classified, category(-C--)'),
+ (140, 1, B'0101', 'this document is classified, category(-C-A)'),
+ (150, 1, B'0110', 'this document is classified, category(-CB-)'),
+ (160, 1, B'0111', 'this document is classified, category(-CBA)'),
+ (170, 2, B'0000', 'this document is secret, category(----)'),
+ (180, 2, B'0001', 'this document is secret, category(---A)'),
+ (190, 2, B'0010', 'this document is secret, category(--B-)'),
+ (200, 2, B'0011', 'this document is secret, category(--BA)'),
+ (210, 2, B'0100', 'this document is secret, category(-C--)'),
+ (220, 2, B'0101', 'this document is secret, category(-C-A)'),
+ (230, 2, B'0110', 'this document is secret, category(-CB-)'),
+ (240, 2, B'0111', 'this document is secret, category(-CBA)');
+
+CREATE TABLE browse (
+ pguser name references account(pguser),
+ did int references document(did),
+ ymd date
+);
+GRANT ALL ON browse TO public;
+INSERT INTO browse VALUES
+ ('rls_regress_user1', 20, '2012-07-01'),
+ ('rls_regress_user1', 40, '2012-07-02'),
+ ('rls_regress_user1', 110, '2012-07-03'),
+ ('rls_regress_user2', 30, '2012-07-04'),
+ ('rls_regress_user2', 50, '2012-07-05'),
+ ('rls_regress_user2', 90, '2012-07-06'),
+ ('rls_regress_user2', 130, '2012-07-07'),
+ ('rls_regress_user2', 150, '2012-07-08'),
+ ('rls_regress_user2', 150, '2012-07-08'),
+ ('rls_regress_user2', 190, '2012-07-09'),
+ ('rls_regress_user2', 210, '2012-07-10'),
+ ('rls_regress_user3', 10, '2012-07-11'),
+ ('rls_regress_user3', 50, '2012-07-12');
+
+-- user's sensitivity level must higher than document's level
+ALTER TABLE document SET ROW LEVEL SECURITY
+ (dlevel <= (SELECT slevel FROM account WHERE pguser = current_user));
+ALTER TABLE browse SET ROW LEVEL SECURITY
+ (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 browse WHERE f_leak(browse::text);
+
+-- viewpoint from rls_regress_user2
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+EXPLAIN SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+
+-- change row-level security policy
+ALTER TABLE document RESET ROW LEVEL SECURITY; -- failed
+ALTER TABLE document SET ROW LEVEL SECURITY (true); -- failed
+
+SET SESSION AUTHORIZATION rls_regress_user0;
+-- switch to category based control from level based
+ALTER TABLE document SET ROW LEVEL SECURITY
+ (dcategory & (SELECT ~scategory FROM account WHERE pguser = current_user) = B'0000');
+
+-- viewpoint from rls_regress_user1 again
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+
+-- viewpoint from rls_regress_user2 again
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+EXPLAIN SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+
+-- Failed to update PK row referenced by invisible FK
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document NATURAL LEFT JOIN browse;
+DELETE FROM document WHERE did = 30; -- failed
+UPDATE document SET did = 9999 WHERE did = 90; -- failed
+
+--
+-- 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;
+
+--
+-- 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)
+
+-- Now COPY TO command does not support RLS
+-- COPY t1 TO stdin;
+
+-- 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);
+
+--
+-- 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;