+ 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 3f61d7d..6cae6c3 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
@@ -806,6 +831,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 49def6a..1b88a96 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"
@@ -142,6 +143,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 */
@@ -171,6 +173,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 */
@@ -1762,6 +1765,8 @@ StartTransaction(void)
/* SecurityRestrictionContext should never be set outside a transaction */
Assert(s->prevSecContext == 0);
+ s->prevRowLevelSecMode = getRowLevelSecurityMode();
+
/*
* initialize other subsystems for new transaction
*/
@@ -2293,6 +2298,9 @@ AbortTransaction(void)
*/
SetUserIdAndSecContext(s->prevUser, s->prevSecContext);
+ /* Also, mode setting of row-level security should be restored. */
+ setRowLevelSecurityMode(s->prevRowLevelSecMode);
+
/*
* do abort processing
*/
@@ -4186,6 +4194,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...
@@ -4325,6 +4336,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 62fc9b0..de840b2 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 98ce598..d18584b 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -45,6 +45,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"
@@ -1221,6 +1222,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);
@@ -2269,6 +2274,9 @@ getObjectClass(const ObjectAddress *object)
case ExtensionRelationId:
return OCLASS_EXTENSION;
+
+ case RowLevelSecurityRelationId:
+ return OCLASS_ROWLEVELSEC;
}
/* shouldn't get here */
@@ -2903,6 +2911,19 @@ 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 c91df90..0d806de 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -775,6 +775,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..bd89c48
--- /dev/null
+++ b/src/backend/catalog/pg_rowlevelsec.c
@@ -0,0 +1,250 @@
+/* -------------------------------------------------------------------------
+ *
+ * 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 "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),
+ "ROW LEVEL SECURITY");
+ /* No aggregate function support */
+ if (pstate->p_hasAggs)
+ ereport(ERROR,
+ (errcode(ERRCODE_GROUPING_ERROR),
+ errmsg("cannot use aggregate in row-level security")));
+ /* No window function support */
+ if (pstate->p_hasWindowFuncs)
+ ereport(ERROR,
+ (errcode(ERRCODE_WINDOWING_ERROR),
+ errmsg("cannot use window function in 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 d69809a..6aae6f8 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;
@@ -7558,6 +7570,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:
@@ -9656,6 +9684,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 df76341..dabcfc6 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -32,6 +32,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"
@@ -163,6 +164,13 @@ 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.
+ */
/* Determine what fraction of the plan is likely to be scanned */
if (cursorOptions & CURSOR_OPT_FAST_PLAN)
@@ -240,6 +248,7 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams)
result->relationOids = glob->relationOids;
result->invalItems = glob->invalItems;
result->nParamExec = list_length(glob->paramlist);
+ result->planUserId = glob->planUserId;
return result;
}
@@ -314,6 +323,14 @@ subquery_planner(PlannerGlobal *glob, Query *parse,
SS_process_ctes(root);
/*
+ * Apply row-level security policy of appeared tables, if configured.
+ * It must be applied prior to preprocess_rowmarks().
+ *
+ *
+ */
+ applyRowLevelSecurity(root);
+
+ /*
* Look for ANY and EXISTS SubLinks in WHERE and JOIN/ON clauses, and try
* to transform them into joins. Note that this step does not descend
* into subqueries; if we pull up any subqueries below, their SubLinks are
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..70a1336
--- /dev/null
+++ b/src/backend/optimizer/util/rowlevelsec.c
@@ -0,0 +1,677 @@
+/*
+ * 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/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 "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;
+
+/*
+ * 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;
+}
+
+/*
+ * pull_rowlevel_security_policy
+ *
+ * This routine tries to pull expression node of row-level security policy
+ * on the target relation, and its children if configured.
+ * If one or more relation has a security policy at least, this function
+ * returns true, or false elsewhere.
+ */
+static bool
+pull_rowlevel_security_policy(PlannerInfo *root,
+ RangeTblEntry *rte,
+ Index rtindex,
+ List **rls_relids,
+ List **rls_quals,
+ List **rls_flags)
+{
+ Relation rel;
+ LOCKMODE lockmode;
+ List *relid_list = NIL;
+ List *qual_list = NIL;
+ List *flag_list = NIL;
+ ListCell *cell;
+ bool result = false;
+
+ Assert(rte->rtekind == RTE_RELATION && OidIsValid(rte->relid));
+
+ 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 == root->parse->resultRelation)
+ lockmode = RowExclusiveLock;
+ else
+ {
+ foreach (cell, root->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);
+
+ /*
+ * 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 *qual_list;
+
+ qual_list = (*rowlevel_security_hook)(root, rel);
+ if (qual_list != NIL)
+ {
+ if ((flags & RLS_FLAG_HAS_SUBLINKS) == 0 &&
+ contain_subplans((Node *)qual_list))
+ flags |= RLS_FLAG_HAS_SUBLINKS;
+
+ if (qual != NULL)
+ qual_list = lappend(qual_list, qual);
+
+ if (list_length(qual_list) == 1)
+ qual = (Expr *)list_head(qual_list);
+ else
+ qual = makeBoolExpr(AND_EXPR, qual_list, -1);
+ }
+ }
+
+ qual_list = lappend(qual_list, qual);
+ if (qual)
+ result = true;
+ flag_list = lappend_int(flag_list, flags);
+
+ heap_close(rel, NoLock); /* close the relation, but keep locks */
+ }
+
+ /*
+ * Inform the caller list of relation Oid, qualifier of row-level
+ * security policy and its flag, if one or more target relations
+ * have its row-level security policy. Elsewhere, release it.
+ */
+ if (result)
+ {
+ *rls_relids = relid_list;
+ *rls_quals = qual_list;
+ *rls_flags = flag_list;
+ }
+ else
+ {
+ list_free(relid_list);
+ list_free(qual_list);
+ list_free(flag_list);
+ }
+ return result;
+}
+
+/*
+ * 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 {
+ PlannerInfo *root;
+ int varlevelsup;
+} fixup_var_context;
+
+static bool
+fixup_var_references_walker(Node *node, fixup_var_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)
+ return false;
+
+ rte = rt_fetch(var->varno, context->root->parse->rtable);
+ if (!rte)
+ elog(ERROR, "invalid varno %d", var->varno);
+
+ if (rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_LEVEL_SECURITY)
+ {
+ List *targetList = rte->subquery->targetList;
+ ListCell *cell;
+
+ foreach (cell, targetList)
+ {
+ TargetEntry *subtle = lfirst(cell);
+
+ if ((IsA(subtle->expr, ConvertRowtypeExpr) &&
+ var->varattno == InvalidAttrNumber) ||
+ (IsA(subtle->expr, Var) &&
+ var->varattno == ((Var *)(subtle->expr))->varattno))
+ {
+ var->varattno = subtle->resno;
+ return false;
+ }
+ }
+ elog(ERROR, "invalid varattno %d", var->varattno);
+ }
+ return false;
+ }
+ else if (IsA(node, Query))
+ {
+ bool result;
+
+ context->varlevelsup++;
+ result = query_tree_walker((Query *) node,
+ fixup_var_references_walker,
+ (void *) context, 0);
+ context->varlevelsup--;
+
+ return result;
+ }
+ return expression_tree_walker(node,
+ fixup_var_references_walker,
+ (void *) context);
+}
+
+static void
+fixup_varattnos(PlannerInfo *root)
+{
+ fixup_var_context context;
+
+ /*
+ * Fixup Var->varattno that references the sub-queries originated from
+ * regular relations with RLS policy.
+ */
+ context.root = root;
+ context.varlevelsup = 0;
+
+ query_tree_walker(root->parse,
+ fixup_var_references_walker,
+ (void *) &context, 0);
+}
+
+/*
+ * make_pseudo_column
+ *
+ * make a TargetEntry object which references a particular column of
+ * the underlying table.
+ */
+static TargetEntry *
+make_pseudo_column(Oid relid_head, Oid relid, AttrNumber attnum)
+{
+ Form_pg_attribute attform;
+ HeapTuple tuple;
+ Var *var;
+ char *resname;
+
+ if (attnum == InvalidAttrNumber)
+ {
+ ConvertRowtypeExpr *r = makeNode(ConvertRowtypeExpr);
+
+ r->arg = (Expr *) makeVar((Index) 1,
+ InvalidAttrNumber,
+ get_rel_type_id(relid),
+ -1,
+ InvalidOid,
+ 0);
+ r->resulttype = get_rel_type_id(relid_head);
+ r->convertformat = COERCE_IMPLICIT_CAST;
+ r->location = -1;
+
+ return makeTargetEntry((Expr *) r, -1, get_rel_name(relid), false);
+ }
+
+ tuple = SearchSysCache2(ATTNUM,
+ ObjectIdGetDatum(relid),
+ Int16GetDatum(attnum));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for attribute %d of relation %u",
+ attnum, relid);
+ attform = (Form_pg_attribute) GETSTRUCT(tuple);
+
+ var = makeVar((Index) 1,
+ attform->attnum,
+ attform->atttypid,
+ attform->atttypmod,
+ InvalidOid,
+ 0);
+ resname = pstrdup(NameStr(attform->attname));
+
+ ReleaseSysCache(tuple);
+
+ return makeTargetEntry((Expr *)var, -1, resname, false);
+}
+
+/*
+ * 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(Oid relid_head,
+ Oid relid,
+ Node *qual,
+ int flags,
+ List *targetList,
+ RangeTblEntry *rte,
+ RowMarkClause *rclause)
+{
+ Query *subqry;
+ RangeTblEntry *subrte;
+ RangeTblRef *subrtr;
+ List *colnameList = NIL;
+ ListCell *cell;
+
+ 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 = rte->requiredPerms;
+ subrte->selectedCols = NULL;
+ subrte->modifiedCols = NULL;
+
+ foreach (cell, targetList)
+ {
+ TargetEntry *oldtle = lfirst(cell);
+ TargetEntry *subtle;
+ AttrNumber attnum;
+
+ if (IsA(oldtle->expr, ConvertRowtypeExpr))
+ attnum = InvalidAttrNumber;
+ else
+ {
+ Assert(IsA(oldtle->expr, Var));
+
+ attnum = get_attnum(relid, oldtle->resname);
+ if (attnum == InvalidAttrNumber)
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_COLUMN),
+ errmsg("column \"%s\" of relation \"%s\" does not exist",
+ oldtle->resname, get_rel_name(relid))));
+ }
+ subtle = make_pseudo_column(relid_head, relid, attnum);
+ subtle->resno = oldtle->resno;
+ subqry->targetList = lappend(subqry->targetList, subtle);
+
+ colnameList = lappend(colnameList,
+ makeString(pstrdup(subtle->resname)));
+ attnum -= FirstLowInvalidHeapAttributeNumber;
+ subrte->selectedCols = bms_add_member(rte->selectedCols, attnum);
+ }
+ subrte->eref = makeAlias(get_rel_name(relid), colnameList);
+
+ 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);
+ subqry->hasForUpdate = true;
+ }
+ return subqry;
+}
+
+/*
+ * expand_rtentry_with_policy
+ *
+ * This routine expand reference to the given RangeTblEntry by a sub-query
+ * which simply references the target relation with the qualifier of row-
+ * level security policy.
+ */
+static void
+expand_rtentry_with_policy(PlannerInfo *root,
+ RangeTblEntry *rte,
+ Index rtindex,
+ List *rls_relids,
+ List *rls_quals,
+ List *rls_flags)
+{
+ Query *parse = root->parse;
+ Oid relid_head;
+ Query *subqry;
+ RangeTblEntry *subrte;
+ TargetEntry *subtle;
+ List *targetList;
+ List *colnameList;
+ Bitmapset *attr_used;
+ AttrNumber attnum;
+ RowMarkClause *rclause;
+ ListCell *cell1;
+ ListCell *cell2;
+ ListCell *cell3;
+ ListCell *lc;
+
+ Assert(rte->rtekind == RTE_RELATION && OidIsValid(rte->relid));
+ Assert(list_length(rls_relids) == list_length(rls_quals));
+ Assert(list_length(rls_relids) == list_length(rls_flags));
+ Assert(list_length(rls_relids) > 0);
+
+ /*
+ * Construct a target-entry list
+ */
+ targetList = NIL;
+ colnameList = NIL;
+ relid_head = linitial_oid(rls_relids);
+ attr_used = bms_union(rte->selectedCols,
+ rte->modifiedCols);
+ while ((attnum = bms_first_member(attr_used)) >= 0)
+ {
+ attnum += FirstLowInvalidHeapAttributeNumber;
+
+ subtle = make_pseudo_column(relid_head, relid_head, attnum);
+ subtle->resno = list_length(targetList) + 1;
+
+ targetList = lappend(targetList, subtle);
+ colnameList = lappend(colnameList,
+ makeString(pstrdup(subtle->resname)));
+ }
+ bms_free(attr_used);
+
+ /*
+ * Push-down row-level lock of the target relation, since sub-query
+ * does not support FOR SHARE/FOR UPDATE locks being assigned.
+ */
+ rclause = NULL;
+ foreach (lc, parse->rowMarks)
+ {
+ if (((RowMarkClause *) lfirst(lc))->rti == rtindex)
+ {
+ rclause = lfirst(lc);
+ parse->rowMarks = list_delete(parse->rowMarks, rclause);
+ break;
+ }
+ }
+
+ /*
+ * Construct sub-query structures
+ */
+ forthree (cell1, rls_relids, cell2, rls_quals, cell3, rls_flags)
+ {
+ Oid relid = lfirst_oid(cell1);
+ Node *qual = lfirst(cell2);
+ int flags = lfirst_int(cell3);
+
+ subqry = make_pseudo_subquery(relid_head, relid, qual, flags,
+ targetList, rte, rclause);
+ if (cell1 == list_head(rls_relids))
+ {
+ Assert(rte->relid == relid);
+ Assert(rte->inh == true || list_length(rls_relids) == 1);
+
+ rte->relid = InvalidOid;
+ rte->rtekind = RTE_SUBQUERY;
+ rte->subquery = subqry;
+ rte->security_barrier = true;
+
+ /* no permission checks are needed to subquery itself */
+ rte->requiredPerms = 0;
+ rte->checkAsUser = InvalidOid;
+ rte->selectedCols = NULL;
+ rte->modifiedCols = NULL;
+
+ rte->alias = NULL;
+ rte->eref = makeAlias(get_rel_name(relid),
+ copyObject(colnameList));
+ if (list_length(rls_relids) == 1)
+ rte->inh = false;
+ }
+
+ /*
+ * RTE's for child relations and AppendRelInfo in case when
+ * the target relation has its children.
+ */
+ if (list_length(rls_relids) > 1)
+ {
+ AppendRelInfo *apinfo;
+ AttrNumber child_rtindex;
+ ListCell *l;
+
+ /*
+ * XXX - Set up RangeTblEntry for the child relation.
+ * Note that it does not need to have security_barrier
+ * attribute, if no row-level security policy is
+ * configured on.
+ */
+ subrte = makeNode(RangeTblEntry);
+ subrte->rtekind = RTE_SUBQUERY;
+ subrte->subquery = subqry;
+ if (qual)
+ subrte->security_barrier = true;
+ subrte->alias = NULL;
+ subrte->eref = makeAlias(get_rel_name(relid),
+ copyObject(colnameList));
+ parse->rtable = lappend(parse->rtable, subrte);
+ child_rtindex = list_length(parse->rtable);
+
+ /*
+ * reference to inherited children performs as if simple
+ * UNION ALL operation, so add AppendRelInfo here.
+ */
+ apinfo = makeNode(AppendRelInfo);
+ apinfo->parent_relid = rtindex;
+ apinfo->child_relid = child_rtindex;
+ foreach (l, targetList)
+ {
+ Var *trans_var
+ = makeVarFromTargetEntry(child_rtindex, lfirst(l));
+ apinfo->translated_vars
+ = lappend(apinfo->translated_vars, trans_var);
+ }
+ root->append_rel_list = lappend(root->append_rel_list, apinfo);
+ }
+ }
+}
+
+/*
+ * applyRowLevelSecurity
+ *
+ * It tries to apply row-level security policy of the relation.
+ * If and when a particular policy is configured on the referenced
+ * relation, it shall be replaced by a sub-query with security-barrier flag;
+ * that references the relation with row-level security policy.
+ * In the result, all users can see is rows of the relation that satisfies
+ * the condition supplied as security policy.
+ */
+void
+applyRowLevelSecurity(PlannerInfo *root)
+{
+ Query *parse = root->parse;
+ ListCell *cell;
+ Index rtindex;
+ bool has_rowlevel_sec = false;
+
+ /* mode checks */
+ if (rowlevel_security_mode == RowLevelSecModeDisabled)
+ return;
+
+ /*
+ * No need to apply row-level security on sub-query being originated
+ * from regular relation with RLS policy any more.
+ */
+ if (parse->querySource == QSRC_ROW_LEVEL_SECURITY)
+ return;
+
+ rtindex = 0;
+ foreach (cell, parse->rtable)
+ {
+ RangeTblEntry *rte = lfirst(cell);
+ List *rls_relids;
+ List *rls_quals;
+ List *rls_flags;
+
+ rtindex++;
+
+ if (rte->rtekind != RTE_RELATION)
+ continue;
+
+ /*
+ * XXX - In this revision, we have no support for UPDATE / DELETE
+ * statement, so simply skip it.
+ */
+ if (rtindex == parse->resultRelation)
+ continue;
+
+ /*
+ * In case when a row-level security policy was configured on
+ * the table referenced by this RangeTblEntry or its children,
+ * it shall be rewritten to sub-query with this policy.
+ */
+ if (pull_rowlevel_security_policy(root, rte, rtindex,
+ &rls_relids, &rls_quals, &rls_flags))
+ {
+ expand_rtentry_with_policy(root, rte, rtindex,
+ rls_relids, rls_quals, rls_flags);
+ has_rowlevel_sec = true;
+ }
+ }
+
+ /*
+ * Post case handling if one or more relation was replaced to sub-query.
+ */
+ if (has_rowlevel_sec)
+ {
+ PlanInvalItem *pi_item;
+
+ /*
+ * plan should be invalidated if and when userid was changed
+ * on the executor stage, from planner stage.
+ */
+ Assert(!OidIsValid(root->glob->planUserId) ||
+ root->glob->planUserId == GetUserId());
+ root->glob->planUserId = GetUserId();
+
+ pi_item = makeNode(PlanInvalItem);
+ pi_item->cacheId = AUTHOID;
+ pi_item->hashValue
+ = GetSysCacheHashValue1(AUTHOID,
+ ObjectIdGetDatum(root->glob->planUserId));
+ root->glob->invalItems = lappend(root->glob->invalItems, pi_item);
+
+ /*
+ * Since the relation with RLS policy was replaced by a sub-query,
+ * thus resource number to reference a particular column can be
+ * also moditifed. If we applied RLS policy on one or more relations,
+ * varattno of Var node that has referenced the rewritten relation
+ * needs to be fixed up.
+ */
+ fixup_varattnos(root);
+ }
+}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 7e6ceed..8c0a8a4 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -2058,6 +2058,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/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index 983f631..c53cbfd 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -42,6 +42,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"
@@ -2998,6 +2999,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];
@@ -3080,6 +3082,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,
@@ -3089,6 +3100,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 c42765c..5ebd944 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"
@@ -664,6 +665,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.
*/
@@ -715,6 +726,8 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
{
CachedPlan *plan;
List *plist;
+ ListCell *cell;
+ Oid planUserId = InvalidOid;
bool snapshot_set;
bool spi_pushed;
MemoryContext plan_context;
@@ -793,6 +806,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.
@@ -827,6 +858,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 2e6776e..11051ed 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -48,6 +48,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"
@@ -895,6 +896,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
*/
@@ -1784,6 +1790,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);
}
@@ -3023,7 +3031,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);
@@ -4173,6 +4187,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 7d67287..0291590 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -3865,6 +3865,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");
@@ -3889,7 +3890,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
@@ -3909,7 +3948,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 "
@@ -3945,7 +3985,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 "
@@ -3980,7 +4021,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 "
@@ -4015,7 +4057,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 "
@@ -4051,7 +4094,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 "
@@ -4086,7 +4130,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 "
@@ -4117,7 +4162,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",
@@ -4143,7 +4189,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",
@@ -4179,7 +4226,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",
@@ -4227,6 +4275,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)
{
@@ -4269,6 +4318,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))
{
@@ -12707,6 +12760,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 b44187bb..20803ab 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -255,6 +255,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 f0eb564..fef31b7 100644
--- a/src/include/catalog/dependency.h
+++ b/src/include/catalog/dependency.h
@@ -146,6 +146,7 @@ typedef enum ObjectClass
OCLASS_USER_MAPPING, /* pg_user_mapping */
OCLASS_DEFACL, /* pg_default_acl */
OCLASS_EXTENSION, /* pg_extension */
+ 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 450ec25..5e4467d 100644
--- a/src/include/catalog/indexing.h
+++ b/src/include/catalog/indexing.h
@@ -306,6 +306,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 50111cb..de85fff 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 */
@@ -1227,6 +1228,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 cf0bbd9..26f986a 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..9afe00a
--- /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/relation.h"
+#include "utils/rel.h"
+
+typedef List *(*rowlevel_security_hook_type)(PlannerInfo *root,
+ 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(PlannerInfo *root);
+
+#endif /* ROWLEVELSEC_H */
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..5673e7a
--- /dev/null
+++ b/src/test/regress/expected/rowlevelsec.out
@@ -0,0 +1,753 @@
+--
+-- 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
+ 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: (rls_regress_schema.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 (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
+ 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: (rls_regress_schema.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 (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, b text, c text) WITH OIDS;
+ALTER TABLE t1 DROP COLUMN b; -- just a disturbing factor
+GRANT ALL ON t1 TO public;
+COPY t1 FROM stdin WITH (oids);
+CREATE TABLE t2 (d float) INHERITS (t1);
+COPY t2 FROM stdin WITH (oids);
+CREATE TABLE t3 (e text) INHERITS (t1);
+COPY t3 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 | c
+---+-----
+ 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(c);
+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 | c
+---+-----
+ 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(c);
+ QUERY PLAN
+-------------------------------------------
+ Result
+ -> Append
+ -> Subquery Scan on t1
+ Filter: f_leak(t1.c)
+ -> Seq Scan on t1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ Filter: f_leak(t2.c)
+ -> Seq Scan on t2
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+ Filter: f_leak(c)
+(12 rows)
+
+-- reference to system column
+SELECT oid, * FROM t1;
+ oid | a | c
+-----+---+-----
+ 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 | c | t1
+---+-----+---------
+ 2 | bbb | (2,bbb)
+ 4 | ddd | (4,ddd)
+ 1 | abc | (1,abc)
+ 3 | cde | (3,cde)
+ 1 | xxx | (1,xxx)
+ 2 | yyy | (2,yyy)
+ 3 | zzz | (3,zzz)
+(7 rows)
+
+EXPLAIN (costs off) SELECT *,t1 FROM t1;
+ QUERY PLAN
+-------------------------------------------
+ Result
+ -> Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ -> Seq Scan on t2
+ Filter: ((a % 2) = 1)
+ -> Subquery Scan on t3
+ -> Seq Scan on t3
+(10 rows)
+
+-- for share/update lock
+SELECT * FROM t1 FOR SHARE;
+ a | c
+---+-----
+ 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
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ -> LockRows
+ -> Seq Scan on t2
+ Filter: ((a % 2) = 1)
+ -> Subquery Scan on t3
+ -> LockRows
+ -> Seq Scan on t3
+(13 rows)
+
+SELECT * FROM t1 WHERE f_leak(c) 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 | c
+---+-----
+ 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(c) FOR SHARE;
+ QUERY PLAN
+-------------------------------------------------
+ Result
+ -> Append
+ -> Subquery Scan on t1
+ Filter: f_leak(t1.c)
+ -> LockRows
+ -> Seq Scan on t1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ Filter: f_leak(t2.c)
+ -> LockRows
+ -> Seq Scan on t2
+ Filter: ((a % 2) = 1)
+ -> Subquery Scan on t3
+ -> LockRows
+ -> Seq Scan on t3
+ Filter: f_leak(c)
+(16 rows)
+
+-- 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 | c
+---+-----
+ 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(c);
+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 | c
+---+-----
+ 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(c);
+ QUERY PLAN
+---------------------------------
+ Result
+ -> Append
+ -> Seq Scan on t1
+ Filter: f_leak(c)
+ -> Seq Scan on t2 t1
+ Filter: f_leak(c)
+ -> Seq Scan on t3 t1
+ Filter: f_leak(c)
+(8 rows)
+
+-- plan cache should be invalidated
+EXECUTE p1(2);
+ a | c
+---+-----
+ 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 t1
+ Filter: (a <= 2)
+ -> Seq Scan on t3 t1
+ Filter: (a <= 2)
+(8 rows)
+
+PREPARE p2(int) AS SELECT * FROM t1 WHERE a = $1;
+EXECUTE p2(2);
+ a | c
+---+-----
+ 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 t1
+ Filter: (a = 2)
+ -> Seq Scan on t3 t1
+ Filter: (a = 2)
+(8 rows)
+
+-- also, case when privilege switch from superuser
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXECUTE p2(2);
+ a | c
+---+-----
+ 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 7 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 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 7f560d2..fd50d9e 100644
--- a/src/test/regress/expected/sanity_check.out
+++ b/src/test/regress/expected/sanity_check.out
@@ -119,6 +119,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
@@ -164,7 +165,7 @@ SELECT relname, relhasindex
timetz_tbl | f
tinterval_tbl | f
varchar_tbl | f
-(153 rows)
+(154 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 8852e0a..d368504 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
test: misc
# rules cannot run concurrently with any test that creates a view
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 0bc5df7..290b24b 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..a4d1ccd
--- /dev/null
+++ b/src/test/regress/sql/rowlevelsec.sql
@@ -0,0 +1,237 @@
+--
+-- 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, b text, c text) WITH OIDS;
+ALTER TABLE t1 DROP COLUMN b; -- 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 (d 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 (e text) INHERITS (t1);
+COPY t3 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(c);
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(c);
+
+-- 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(c) FOR SHARE;
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(c) FOR SHARE;
+
+-- 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(c);
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(c);
+
+-- 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;