diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
new file mode 100644
index c174e67..a26be35 100644
*** a/doc/src/sgml/catalogs.sgml
--- b/doc/src/sgml/catalogs.sgml
***************
*** 239,244 ****
--- 239,249 ----
+ pg_rowsecuritylevelsec
+ row-level security policy of relation
+
+
+ pg_seclabelsecurity labels on database objects
***************
*** 1924,1929 ****
--- 1929,1944 ----
+ relhasrowsecurity
+ bool
+
+
+ True if table has row-security policy; see
+ pg_rowsecurity catalog
+
+
+
+ relhassubclassbool
***************
*** 5324,5329 ****
--- 5339,5395 ----
+
+ pg_rowsecurity
+
+
+ pg_rowsecurity
+
+
+ The catalog pg_rowsecurity stores the expression
+ tree which is generated and used to enforce row-level security on a particular
+ relation.
+
+
+ pg_rowsecurity Columns
+
+
+
+ Name
+ Type
+ References
+ Description
+
+
+
+
+ rsecrelid
+ oid
+ pg_class.oid
+ The relation that the row-level security expression tree applies to.
+
+
+ rseccmd
+ char
+
+ The command this row-security is for. Currently, only 'a' (meaning 'all') is supported.
+
+
+ rsecqual
+ pg_node_tree
+
+ The expression tree that will be applied to any queries referencing this relation.
+
+
+
+
+
+
+ pg_class.relhasrowsecurity
+ This will be true if a relation has an entry in this catalog, used to determine if RLS needs to be applied.
+
+
+ pg_seclabel
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
new file mode 100644
index 0b08f83..9b11183 100644
*** a/doc/src/sgml/ref/alter_table.sgml
--- b/doc/src/sgml/ref/alter_table.sgml
*************** ALTER TABLE [ IF EXISTS ] new_owner
SET TABLESPACE new_tablespace
+ SET ROW SECURITY FOR rowsec_command TO (condition)
+ RESET ROW SECURITY FOR rowsec_command
REPLICA IDENTITY {DEFAULT | USING INDEX index_name | FULL | NOTHING}
and table_constraint_using_index is:
*************** ALTER TABLE [ IF EXISTS ] constraint_name ]
{ UNIQUE | PRIMARY KEY } USING INDEX index_name
[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+
+ and rowsec_command is:
+ { ALL | SELECT | INSERT | UPDATE | DELETE }
+
*************** ALTER TABLE [ IF EXISTS ]
+
+ SET ROW SECURITY FOR rowsec_command TO (condition)
+
+
+ This form sets a row-level security policy for the table.
+ Only rows where condition returns true
+ will be visible to a given query.
+ Currently, the only the command type supported is ALL>.
+ See also .
+
+
+
+
+
+ RESET ROW SECURITY FOR rowsec_command
+
+
+ This form resets (clears) the row-level security policy for the table.
+ ALL> is the only supported command type right now.
+
+
+
+
REPLICA IDENTITY
*************** ALTER TABLE [ IF EXISTS ]
+
+
+ condition
+
+
+ An expression that returns a value of type boolean. Only rows where
+ the condition return true will be fetched, updated, or deleted. Roles
+ marked as 'superuser' will not have the condition applied.
+ The condition can reference columns of the relation and include sub-queries,
+ provided those sub-queries do not reference the relation recursively.
+
+
+
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
new file mode 100644
index a974bd5..beb73df 100644
*** a/src/backend/catalog/Makefile
--- b/src/backend/catalog/Makefile
*************** OBJS = catalog.o dependency.o heap.o ind
*** 15,21 ****
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
BKIFILES = postgres.bki postgres.description postgres.shdescription
--- 15,21 ----
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_rowsecurity.o pg_type.o storage.o toasting.o
BKIFILES = postgres.bki postgres.description postgres.shdescription
*************** POSTGRES_BKI_SRCS = $(addprefix $(top_sr
*** 39,45 ****
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_default_acl.h pg_seclabel.h pg_shseclabel.h pg_collation.h pg_range.h \
toasting.h indexing.h \
)
--- 39,45 ----
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_rowsecurity.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
new file mode 100644
index e511669..8f95b14 100644
*** a/src/backend/catalog/dependency.c
--- b/src/backend/catalog/dependency.c
***************
*** 45,50 ****
--- 45,51 ----
#include "catalog/pg_opfamily.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_rewrite.h"
+ #include "catalog/pg_rowsecurity.h"
#include "catalog/pg_tablespace.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_ts_config.h"
*************** doDeletion(const ObjectAddress *object,
*** 1249,1254 ****
--- 1250,1259 ----
RemoveEventTriggerById(object->objectId);
break;
+ case OCLASS_ROWSECURITY:
+ RemoveRowSecurityById(object->objectId);
+ break;
+
default:
elog(ERROR, "unrecognized object class: %u",
object->classId);
*************** getObjectClass(const ObjectAddress *obje
*** 2316,2321 ****
--- 2321,2329 ----
case EventTriggerRelationId:
return OCLASS_EVENT_TRIGGER;
+
+ case RowSecurityRelationId:
+ return OCLASS_ROWSECURITY;
}
/* shouldn't get here */
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
new file mode 100644
index 2cf4bc0..5be195b 100644
*** a/src/backend/catalog/heap.c
--- b/src/backend/catalog/heap.c
*************** InsertPgClassTuple(Relation pg_class_des
*** 799,804 ****
--- 799,805 ----
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_relhasrowsecurity - 1] = BoolGetDatum(rd_rel->relhasrowsecurity);
values[Anum_pg_class_relhassubclass - 1] = BoolGetDatum(rd_rel->relhassubclass);
values[Anum_pg_class_relispopulated - 1] = BoolGetDatum(rd_rel->relispopulated);
values[Anum_pg_class_relreplident - 1] = CharGetDatum(rd_rel->relreplident);
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
new file mode 100644
index ea81223..083284f 100644
*** a/src/backend/catalog/objectaddress.c
--- b/src/backend/catalog/objectaddress.c
*************** getObjectDescription(const ObjectAddress
*** 2166,2171 ****
--- 2166,2221 ----
break;
}
+ case OCLASS_ROWSECURITY:
+ {
+ Relation rsec_rel;
+ ScanKeyData skey;
+ SysScanDesc sscan;
+ HeapTuple tuple;
+ Form_pg_rowsecurity form_rsec;
+
+ rsec_rel = heap_open(RowSecurityRelationId, AccessShareLock);
+
+ ScanKeyInit(&skey,
+ ObjectIdAttributeNumber,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(object->objectId));
+ sscan = systable_beginscan(rsec_rel, RowSecurityOidIndexId,
+ true, NULL, 1, &skey);
+ tuple = systable_getnext(sscan);
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for row-security relation %u",
+ object->objectId);
+ form_rsec = (Form_pg_rowsecurity) GETSTRUCT(tuple);
+
+ appendStringInfo(&buffer, _("row-security of "));
+ getRelationDescription(&buffer, form_rsec->rsecrelid);
+ switch (form_rsec->rseccmd)
+ {
+ case ROWSECURITY_CMD_ALL:
+ appendStringInfo(&buffer, _(" FOR ALL"));
+ break;
+ case ROWSECURITY_CMD_SELECT:
+ appendStringInfo(&buffer, _(" FOR SELECT"));
+ break;
+ case ROWSECURITY_CMD_INSERT:
+ appendStringInfo(&buffer, _(" FOR INSERT"));
+ break;
+ case ROWSECURITY_CMD_UPDATE:
+ appendStringInfo(&buffer, _(" FOR UPDATE"));
+ break;
+ case ROWSECURITY_CMD_DELETE:
+ appendStringInfo(&buffer, _(" FOR DELETE"));
+ break;
+ default:
+ elog(ERROR, "unrecognized row-security command type: %c",
+ form_rsec->rseccmd);
+ }
+ systable_endscan(sscan);
+ heap_close(rsec_rel, AccessShareLock);
+ break;
+ }
+
default:
appendStringInfo(&buffer, "unrecognized object %u %u %d",
object->classId,
diff --git a/src/backend/catalog/pg_rowsecurity.c b/src/backend/catalog/pg_rowsecurity.c
new file mode 100644
index ...6f55654 .
*** a/src/backend/catalog/pg_rowsecurity.c
--- b/src/backend/catalog/pg_rowsecurity.c
***************
*** 0 ****
--- 1,351 ----
+ /* -------------------------------------------------------------------------
+ *
+ * pg_rowsecurity.c
+ * routines to support manipulation of the pg_rowsecurity catalog
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * -------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+ #include "access/genam.h"
+ #include "access/heapam.h"
+ #include "access/htup_details.h"
+ #include "access/sysattr.h"
+ #include "catalog/dependency.h"
+ #include "catalog/indexing.h"
+ #include "catalog/pg_class.h"
+ #include "catalog/pg_rowsecurity.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/inval.h"
+ #include "utils/rel.h"
+ #include "utils/syscache.h"
+ #include "utils/tqual.h"
+
+ /*
+ * Load row-security policy from the catalog, and keep it in
+ * the relation cache.
+ */
+ void
+ RelationBuildRowSecurity(Relation relation)
+ {
+ Relation catalog;
+ ScanKeyData skey;
+ SysScanDesc sscan;
+ HeapTuple tuple;
+ MemoryContext oldcxt;
+ MemoryContext rscxt = NULL;
+ RowSecurityDesc *rsdesc = NULL;
+
+ catalog = heap_open(RowSecurityRelationId, AccessShareLock);
+
+ ScanKeyInit(&skey,
+ Anum_pg_rowsecurity_rsecrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(RelationGetRelid(relation)));
+ sscan = systable_beginscan(catalog, RowSecurityRelidIndexId, true,
+ NULL, 1, &skey);
+ PG_TRY();
+ {
+ /*
+ * Loop through the row-level security entries for this relation, if
+ * any. While we currently only support one command type for row-level
+ * security, eventually we will support multiple types and we will
+ * need to find the correct one (or possibly merge them?).
+ */
+ while (HeapTupleIsValid(tuple = systable_getnext(sscan)))
+ {
+ Datum value;
+ bool isnull;
+ char *temp;
+
+ value = heap_getattr(tuple, Anum_pg_rowsecurity_rseccmd,
+ RelationGetDescr(catalog), &isnull);
+ Assert(!isnull);
+
+ if (DatumGetChar(value) != ROWSECURITY_CMD_ALL)
+ {
+ ereport(WARNING,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("per-command row-security not implemented")));
+ continue;
+ }
+
+ /*
+ * Set up the memory context inside our loop to ensure we are only
+ * building it when we actually need it.
+ */
+ if (!rsdesc)
+ {
+ rscxt = AllocSetContextCreate(CacheMemoryContext,
+ "Row-security descriptor",
+ ALLOCSET_SMALL_MINSIZE,
+ ALLOCSET_SMALL_INITSIZE,
+ ALLOCSET_SMALL_MAXSIZE);
+ rsdesc = MemoryContextAllocZero(rscxt, sizeof(RowSecurityDesc));
+ rsdesc->rscxt = rscxt;
+ }
+
+ value = heap_getattr(tuple, Anum_pg_rowsecurity_rsecqual,
+ RelationGetDescr(catalog), &isnull);
+ Assert(!isnull);
+
+ temp = TextDatumGetCString(value);
+
+ oldcxt = MemoryContextSwitchTo(rscxt);
+
+ rsdesc->rsall.rsecid = HeapTupleGetOid(tuple);
+ rsdesc->rsall.qual = (Expr *) stringToNode(temp);
+ Assert(exprType((Node *)rsdesc->rsall.qual) == BOOLOID);
+ rsdesc->rsall.hassublinks
+ = contain_subplans((Node *)rsdesc->rsall.qual);
+
+ MemoryContextSwitchTo(oldcxt);
+
+ pfree(temp);
+ }
+ }
+ PG_CATCH();
+ {
+ if (rscxt != NULL)
+ MemoryContextDelete(rscxt);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ systable_endscan(sscan);
+ heap_close(catalog, AccessShareLock);
+
+ relation->rsdesc = rsdesc;
+ }
+
+ /*
+ * Parse the supplied row-security policy, and insert or update the row
+ * in pg_rowsecurity catalog.
+ */
+ static void
+ InsertOrUpdatePolicyRow(Relation relation, char rseccmd, Node *clause)
+ {
+ Oid relationId = RelationGetRelid(relation);
+ Oid rowsecId;
+ ParseState *pstate;
+ RangeTblEntry *rte;
+ Node *qual;
+ Relation catalog;
+ ScanKeyData skeys[2];
+ SysScanDesc sscan;
+ HeapTuple oldtup;
+ HeapTuple newtup;
+ Datum values[Natts_pg_rowsecurity];
+ bool isnull[Natts_pg_rowsecurity];
+ bool replaces[Natts_pg_rowsecurity];
+ ObjectAddress target;
+ ObjectAddress myself;
+
+ /* Parse the supplied clause */
+ pstate = make_parsestate(NULL);
+
+ rte = addRangeTableEntryForRelation(pstate, relation,
+ NULL, false, false);
+ addRTEtoQuery(pstate, rte, false, true, true);
+
+ qual = transformWhereClause(pstate, copyObject(clause),
+ EXPR_KIND_ROW_SECURITY,
+ "ROW SECURITY");
+ /* zero-clear */
+ memset(values, 0, sizeof(values));
+ memset(replaces, 0, sizeof(replaces));
+ memset(isnull, 0, sizeof(isnull));
+
+ /* Update or Insert an entry to pg_rowsecurity catalog */
+ catalog = heap_open(RowSecurityRelationId, RowExclusiveLock);
+
+ ScanKeyInit(&skeys[0],
+ Anum_pg_rowsecurity_rsecrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(RelationGetRelid(relation)));
+ ScanKeyInit(&skeys[1],
+ Anum_pg_rowsecurity_rseccmd,
+ BTEqualStrategyNumber, F_CHAREQ,
+ CharGetDatum(rseccmd));
+ sscan = systable_beginscan(catalog, RowSecurityRelidIndexId, true,
+ NULL, 2, skeys);
+
+ oldtup = systable_getnext(sscan);
+ if (HeapTupleIsValid(oldtup))
+ {
+ rowsecId = HeapTupleGetOid(oldtup);
+
+ replaces[Anum_pg_rowsecurity_rsecqual - 1] = true;
+ values[Anum_pg_rowsecurity_rsecqual - 1]
+ = CStringGetTextDatum(nodeToString(qual));
+
+ newtup = heap_modify_tuple(oldtup,
+ RelationGetDescr(catalog),
+ values, isnull, replaces);
+ simple_heap_update(catalog, &newtup->t_self, newtup);
+
+ /* We will be adding them back below. */
+ deleteDependencyRecordsFor(RowSecurityRelationId, rowsecId, false);
+ }
+ else
+ {
+ values[Anum_pg_rowsecurity_rsecrelid - 1]
+ = ObjectIdGetDatum(relationId);
+ values[Anum_pg_rowsecurity_rseccmd - 1]
+ = CharGetDatum(rseccmd);
+ values[Anum_pg_rowsecurity_rsecqual - 1]
+ = CStringGetTextDatum(nodeToString(qual));
+ newtup = heap_form_tuple(RelationGetDescr(catalog),
+ values, isnull);
+ rowsecId = simple_heap_insert(catalog, newtup);
+ }
+ CatalogUpdateIndexes(catalog, newtup);
+
+ heap_freetuple(newtup);
+
+ /* records dependencies of row-security policy and relation/columns */
+ target.classId = RelationRelationId;
+ target.objectId = relationId;
+ target.objectSubId = 0;
+
+ myself.classId = RowSecurityRelationId;
+ myself.objectId = rowsecId;
+ myself.objectSubId = 0;
+
+ recordDependencyOn(&myself, &target, DEPENDENCY_AUTO);
+
+ recordDependencyOnExpr(&myself, qual, pstate->p_rtable,
+ DEPENDENCY_NORMAL);
+ free_parsestate(pstate);
+
+ systable_endscan(sscan);
+ heap_close(catalog, RowExclusiveLock);
+ }
+
+ /*
+ * Remove row-security policy row of pg_rowsecurity
+ */
+ static void
+ DeletePolicyRow(Relation relation, char rseccmd)
+ {
+ Assert(rseccmd == ROWSECURITY_CMD_ALL);
+
+ if (relation->rsdesc)
+ {
+ ObjectAddress address;
+
+ address.classId = RowSecurityRelationId;
+ address.objectId = relation->rsdesc->rsall.rsecid;
+ address.objectSubId = 0;
+
+ performDeletion(&address, DROP_RESTRICT, 0);
+ }
+ else
+ {
+ /* Nothing to do here */
+ elog(INFO, "relation %s has no row-security policy, skipped",
+ RelationGetRelationName(relation));
+ }
+ }
+
+ /*
+ * Guts of row-security policy deletion.
+ */
+ void
+ RemoveRowSecurityById(Oid rowsecId)
+ {
+ Relation catalog;
+ ScanKeyData skey;
+ SysScanDesc sscan;
+ HeapTuple tuple;
+ Relation rel;
+ Oid relid;
+
+ catalog = heap_open(RowSecurityRelationId, RowExclusiveLock);
+
+ ScanKeyInit(&skey,
+ ObjectIdAttributeNumber,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(rowsecId));
+ sscan = systable_beginscan(catalog, RowSecurityOidIndexId, true,
+ NULL, 1, &skey);
+ tuple = systable_getnext(sscan);
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "could not find tuple for row-security %u", rowsecId);
+
+ /*
+ * Open and exclusive-lock the relation the row-security belongs to.
+ */
+ relid = ((Form_pg_rowsecurity) GETSTRUCT(tuple))->rsecrelid;
+
+ rel = heap_open(relid, AccessExclusiveLock);
+
+ simple_heap_delete(catalog, &tuple->t_self);
+
+ /* Ensure relcache entries of other session being rebuilt */
+ CacheInvalidateRelcache(rel);
+
+ heap_close(rel, NoLock);
+
+ systable_endscan(sscan);
+ heap_close(catalog, RowExclusiveLock);
+ }
+
+ /*
+ * ALTER TABLE SET ROW SECURITY (...) OR
+ * RESET ROW SECURITY
+ */
+ void
+ ATExecSetRowSecurity(Relation relation, const char *cmdname, Node *clause)
+ {
+ Oid relid = RelationGetRelid(relation);
+ char rseccmd;
+
+ if (strcmp(cmdname, "all") == 0)
+ rseccmd = ROWSECURITY_CMD_ALL;
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("Row-security for \"%s\" is not implemented yet",
+ cmdname)));
+
+ if (clause != NULL)
+ {
+ InsertOrUpdatePolicyRow(relation, rseccmd, clause);
+
+ /*
+ * Also, turn on relhasrowsecurity, if not.
+ */
+ if (!RelationGetForm(relation)->relhasrowsecurity)
+ {
+ Relation class_rel = heap_open(RelationRelationId,
+ RowExclusiveLock);
+ HeapTuple tuple;
+
+ tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for relation %u", relid);
+
+ ((Form_pg_class) GETSTRUCT(tuple))->relhasrowsecurity = true;
+
+ simple_heap_update(class_rel, &tuple->t_self, tuple);
+ CatalogUpdateIndexes(class_rel, tuple);
+
+ heap_freetuple(tuple);
+ heap_close(class_rel, RowExclusiveLock);
+ }
+ }
+ else
+ DeletePolicyRow(relation, rseccmd);
+
+ CacheInvalidateRelcache(relation);
+ }
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
new file mode 100644
index 70ee7e5..4568a47 100644
*** a/src/backend/commands/copy.c
--- b/src/backend/commands/copy.c
***************
*** 34,39 ****
--- 34,40 ----
#include "libpq/pqformat.h"
#include "mb/pg_wchar.h"
#include "miscadmin.h"
+ #include "rewrite/rowsecurity.h"
#include "optimizer/clauses.h"
#include "optimizer/planner.h"
#include "parser/parse_relation.h"
*************** DoCopy(const CopyStmt *stmt, const char
*** 809,814 ****
--- 810,816 ----
RangeTblEntry *rte;
List *attnums;
ListCell *cur;
+ List *rowsecpolicy;
Assert(!stmt->query);
*************** DoCopy(const CopyStmt *stmt, const char
*** 816,823 ****
rel = heap_openrv(stmt->relation,
(is_from ? RowExclusiveLock : AccessShareLock));
! relid = RelationGetRelid(rel);
rte = makeNode(RangeTblEntry);
rte->rtekind = RTE_RELATION;
rte->relid = RelationGetRelid(rel);
--- 818,831 ----
rel = heap_openrv(stmt->relation,
(is_from ? RowExclusiveLock : AccessShareLock));
! /* Test for row-security policy. If there's any policy for this
! * relation, we don't permit COPY on it. */
! rowsecpolicy = pull_row_security_policy(CMD_UTILITY, rel, NULL);
! if (rowsecpolicy != NIL)
! ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
! errmsg("Cannot COPY a relation with a row-security policy as non-superuser")));
+ relid = RelationGetRelid(rel);
rte = makeNode(RangeTblEntry);
rte->rtekind = RTE_RELATION;
rte->relid = RelationGetRelid(rel);
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
new file mode 100644
index 024a477..220e731 100644
*** a/src/backend/commands/event_trigger.c
--- b/src/backend/commands/event_trigger.c
*************** EventTriggerSupportsObjectClass(ObjectCl
*** 995,1000 ****
--- 995,1001 ----
case OCLASS_USER_MAPPING:
case OCLASS_DEFACL:
case OCLASS_EXTENSION:
+ case OCLASS_ROWSECURITY:
return true;
case MAX_OCLASS:
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
new file mode 100644
index f5ae98f..9569d64 100644
*** a/src/backend/commands/tablecmds.c
--- b/src/backend/commands/tablecmds.c
***************
*** 37,42 ****
--- 37,43 ----
#include "catalog/pg_inherits_fn.h"
#include "catalog/pg_namespace.h"
#include "catalog/pg_opclass.h"
+ #include "catalog/pg_rowsecurity.h"
#include "catalog/pg_tablespace.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
*************** AlterTableGetLockLevel(List *cmds)
*** 2807,2812 ****
--- 2808,2822 ----
break;
/*
+ * Changing row security changes the set of quals that a
+ * select could be built off of.
+ */
+ case AT_SetRowSecurity:
+ case AT_ResetRowSecurity:
+ cmd_lockmode = AccessExclusiveLock;
+ break;
+
+ /*
* Subcommands that may be visible to concurrent SELECTs
*/
case AT_DropColumn: /* change visible to SELECT */
*************** ATPrepCmd(List **wqueue, Relation rel, A
*** 3234,3239 ****
--- 3244,3251 ----
case AT_DropInherit: /* NO INHERIT */
case AT_AddOf: /* OF */
case AT_DropOf: /* NOT OF */
+ case AT_SetRowSecurity:
+ case AT_ResetRowSecurity:
ATSimplePermissions(rel, ATT_TABLE);
/* These commands never recurse */
/* No command-specific prep needed */
*************** ATExecCmd(List **wqueue, AlteredTableInf
*** 3519,3524 ****
--- 3531,3542 ----
case AT_DropOf:
ATExecDropOf(rel, lockmode);
break;
+ case AT_SetRowSecurity:
+ ATExecSetRowSecurity(rel, cmd->name, (Node *) cmd->def);
+ break;
+ case AT_ResetRowSecurity:
+ ATExecSetRowSecurity(rel, cmd->name, NULL);
+ break;
case AT_ReplicaIdentity:
ATExecReplicaIdentity(rel, (ReplicaIdentityStmt *) cmd->def, lockmode);
break;
*************** ATExecAlterColumnType(AlteredTableInfo *
*** 7889,7894 ****
--- 7907,7928 ----
Assert(defaultexpr);
break;
+ case OCLASS_ROWSECURITY:
+ /*
+ * Since a row-level security policy can reference a column
+ * of the table, we currently just punt on attempts to change
+ * the column type of a relation which has an RLS policy.
+ * See above comments regarding a similar situation with
+ * triggers (the OCLASS_TRIGGER case). FIXME someday.
+ */
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot alter type of a column used in a row-level security policy"),
+ errdetail("%s depends on column \"%s\"",
+ getObjectDescription(&foundObject),
+ colName)));
+ break;
+
case OCLASS_PROC:
case OCLASS_TYPE:
case OCLASS_CAST:
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
new file mode 100644
index 98ad910..8a3a121 100644
*** a/src/backend/nodes/copyfuncs.c
--- b/src/backend/nodes/copyfuncs.c
*************** _copyQuery(const Query *from)
*** 2491,2496 ****
--- 2491,2497 ----
COPY_NODE_FIELD(rowMarks);
COPY_NODE_FIELD(setOperations);
COPY_NODE_FIELD(constraintDeps);
+ COPY_SCALAR_FIELD(dependsUserId);
return newnode;
}
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
new file mode 100644
index 9901d23..0c9637f 100644
*** a/src/backend/nodes/equalfuncs.c
--- b/src/backend/nodes/equalfuncs.c
*************** _equalQuery(const Query *a, const Query
*** 872,877 ****
--- 872,878 ----
COMPARE_NODE_FIELD(rowMarks);
COMPARE_NODE_FIELD(setOperations);
COMPARE_NODE_FIELD(constraintDeps);
+ COMPARE_SCALAR_FIELD(dependsUserId);
return true;
}
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
new file mode 100644
index 10e8139..ce57004 100644
*** a/src/backend/nodes/outfuncs.c
--- b/src/backend/nodes/outfuncs.c
*************** _outQuery(StringInfo str, const Query *n
*** 2264,2269 ****
--- 2264,2270 ----
WRITE_NODE_FIELD(rowMarks);
WRITE_NODE_FIELD(setOperations);
WRITE_NODE_FIELD(constraintDeps);
+ WRITE_OID_FIELD(dependsUserId);
}
static void
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
new file mode 100644
index ef1eae9..1161cb4 100644
*** a/src/backend/nodes/readfuncs.c
--- b/src/backend/nodes/readfuncs.c
*************** _readQuery(void)
*** 224,229 ****
--- 224,230 ----
READ_NODE_FIELD(rowMarks);
READ_NODE_FIELD(setOperations);
READ_NODE_FIELD(constraintDeps);
+ READ_OID_FIELD(dependsUserId);
READ_DONE();
}
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
new file mode 100644
index 0508d16..e1ccc31 100644
*** a/src/backend/optimizer/plan/planner.c
--- b/src/backend/optimizer/plan/planner.c
***************
*** 40,45 ****
--- 40,46 ----
#include "rewrite/rewriteManip.h"
#include "utils/rel.h"
#include "utils/selfuncs.h"
+ #include "utils/syscache.h"
/* GUC parameter */
*************** standard_planner(Query *parse, int curso
*** 177,182 ****
--- 178,184 ----
glob->lastPHId = 0;
glob->lastRowMarkId = 0;
glob->transientPlan = false;
+ glob->planUserId = InvalidOid;
/* Determine what fraction of the plan is likely to be scanned */
if (cursorOptions & CURSOR_OPT_FAST_PLAN)
*************** standard_planner(Query *parse, int curso
*** 254,263 ****
--- 256,305 ----
result->relationOids = glob->relationOids;
result->invalItems = glob->invalItems;
result->nParamExec = glob->nParamExec;
+ result->planUserId = glob->planUserId;
return result;
}
+ static void
+ create_userdep_planinval(Query* parse, PlannerGlobal *glob)
+ {
+ Oid dep = parse->dependsUserId;
+ Oid globdep = glob->planUserId;
+
+ /*
+ * If the global dependency is already set and we have a dependency
+ * for this subtree, make sure it's the same as the one we found on
+ * this subtree. A plan can't depend on multiple user IDs.
+ */
+ Assert( !OidIsValid(globdep) || !OidIsValid(dep) || (globdep == dep) );
+
+ /*
+ * If the global dependency isn't already set, and we have a
+ * user id dependency, copy the oid and create a plan inval
+ * item on pg_authid so that if the user id is updated this plan
+ * gets invalidated (say, due to the user becoming/ceasing to be
+ * superuser).
+ */
+ if (!OidIsValid(globdep) && OidIsValid(dep))
+ {
+ PlanInvalItem *pi = makeNode(PlanInvalItem);
+
+ glob->planUserId = dep;
+
+ pi->cacheId = AUTHOID;
+ pi->hashValue = GetSysCacheHashValue1(AUTHOID,
+ ObjectIdGetDatum(dep));
+ glob->invalItems = lappend(glob->invalItems, pi);
+ }
+
+ /*
+ * Otherwise we have nothing to do. Either the dependency
+ * has already been recognised and invalidation items created,
+ * or there isn't a dependency for this subnode.
+ */
+ }
+
/*--------------------
* subquery_planner
*************** subquery_planner(PlannerGlobal *glob, Qu
*** 323,328 ****
--- 365,377 ----
root->non_recursive_plan = NULL;
/*
+ * Check to see whether anything in the subquery depends on the current user ID.
+ * If it does, set the relevant PlannerGlobal flag and create an inval item
+ * on the catalog.
+ */
+ create_userdep_planinval(parse, glob);
+
+ /*
* If there is a WITH list, process each WITH query and build an initplan
* SubPlan structure for it.
*/
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
new file mode 100644
index 46affe7..ef29b30 100644
*** a/src/backend/optimizer/plan/setrefs.c
--- b/src/backend/optimizer/plan/setrefs.c
*************** record_plan_function_dependency(PlannerI
*** 2040,2046 ****
void
extract_query_dependencies(Node *query,
List **relationOids,
! List **invalItems)
{
PlannerGlobal glob;
PlannerInfo root;
--- 2040,2047 ----
void
extract_query_dependencies(Node *query,
List **relationOids,
! List **invalItems,
! Oid *planUserId)
{
PlannerGlobal glob;
PlannerInfo root;
*************** extract_query_dependencies(Node *query,
*** 2050,2055 ****
--- 2051,2057 ----
glob.type = T_PlannerGlobal;
glob.relationOids = NIL;
glob.invalItems = NIL;
+ glob.planUserId = InvalidOid;
MemSet(&root, 0, sizeof(root));
root.type = T_PlannerInfo;
*************** extract_query_dependencies(Node *query,
*** 2059,2064 ****
--- 2061,2067 ----
*relationOids = glob.relationOids;
*invalItems = glob.invalItems;
+ *planUserId = glob.planUserId;
}
static bool
*************** extract_query_dependencies_walker(Node *
*** 2074,2079 ****
--- 2077,2097 ----
Query *query = (Query *) node;
ListCell *lc;
+ /*
+ * Check to see if this node has a user id dependency; if so,
+ * register it in the global state.
+ */
+ if (OidIsValid(query->dependsUserId))
+ {
+ /*
+ * Different Query nodes may not have the same user id
+ * dependency.
+ */
+ Assert( !OidIsValid(context->glob->planUserId)
+ || context->glob->planUserId == query->dependsUserId );
+ context->glob->planUserId = query->dependsUserId;
+ }
+
if (query->commandType == CMD_UTILITY)
{
/*
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
new file mode 100644
index 7225bb6..0550f63 100644
*** a/src/backend/parser/analyze.c
--- b/src/backend/parser/analyze.c
*************** transformDeleteStmt(ParseState *pstate,
*** 346,351 ****
--- 346,352 ----
Node *qual;
qry->commandType = CMD_DELETE;
+ qry->dependsUserId = InvalidOid;
/* process the WITH clause independently of all else */
if (stmt->withClause)
*************** transformInsertStmt(ParseState *pstate,
*** 428,433 ****
--- 429,435 ----
Assert(pstate->p_ctenamespace == NIL);
qry->commandType = CMD_INSERT;
+ qry->dependsUserId = InvalidOid;
pstate->p_is_insert = true;
/* process the WITH clause independently of all else */
*************** transformSelectStmt(ParseState *pstate,
*** 912,917 ****
--- 914,920 ----
ListCell *l;
qry->commandType = CMD_SELECT;
+ qry->dependsUserId = InvalidOid;
/* process the WITH clause independently of all else */
if (stmt->withClause)
*************** transformValuesClause(ParseState *pstate
*** 1051,1056 ****
--- 1054,1060 ----
int i;
qry->commandType = CMD_SELECT;
+ qry->dependsUserId = InvalidOid;
/* Most SELECT stuff doesn't apply in a VALUES clause */
Assert(stmt->distinctClause == NIL);
*************** transformSetOperationStmt(ParseState *ps
*** 1287,1292 ****
--- 1291,1297 ----
int tllen;
qry->commandType = CMD_SELECT;
+ qry->dependsUserId = InvalidOid;
/*
* Find leftmost leaf SelectStmt. We currently only need to do this in
*************** transformUpdateStmt(ParseState *pstate,
*** 1909,1914 ****
--- 1914,1920 ----
ListCell *tl;
qry->commandType = CMD_UPDATE;
+ qry->dependsUserId = InvalidOid;
pstate->p_is_update = true;
/* process the WITH clause independently of all else */
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
new file mode 100644
index 7b9895d..686750b 100644
*** a/src/backend/parser/gram.y
--- b/src/backend/parser/gram.y
*************** static Node *makeRecursiveViewSelect(cha
*** 258,263 ****
--- 258,264 ----
%type alter_table_cmd alter_type_cmd opt_collate_clause
replica_identity
%type alter_table_cmds alter_type_cmds
+ %type row_security_cmd
%type opt_drop_behavior
*************** alter_table_cmd:
*** 2190,2195 ****
--- 2191,2214 ----
n->def = (Node *)$2;
$$ = (Node *)n;
}
+ /* ALTER TABLE SET ROW SECURITY FOR TO () */
+ | SET ROW SECURITY FOR row_security_cmd TO '(' a_expr ')'
+ {
+ AlterTableCmd *n = makeNode(AlterTableCmd);
+ n->subtype = AT_SetRowSecurity;
+ n->name = $5;
+ n->def = (Node *) $8;
+ $$ = (Node *)n;
+ }
+ /* ALTER TABLE RESET ROW SECURITY FOR */
+ | RESET ROW SECURITY FOR row_security_cmd
+ {
+ AlterTableCmd *n = makeNode(AlterTableCmd);
+ n->subtype = AT_ResetRowSecurity;
+ n->name = $5;
+ n->def = NULL;
+ $$ = (Node *)n;
+ }
/* ALTER TABLE REPLICA IDENTITY */
| REPLICA IDENTITY_P replica_identity
{
*************** reloption_elem:
*** 2300,2305 ****
--- 2319,2330 ----
}
;
+ row_security_cmd: ALL { $$ = "all"; }
+ | SELECT { $$ = "select"; }
+ | INSERT { $$ = "insert"; }
+ | UPDATE { $$ = "update"; }
+ | DELETE_P { $$ = "delete"; }
+ ;
/*****************************************************************************
*
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
new file mode 100644
index 272d27f..091ac68 100644
*** a/src/backend/parser/parse_agg.c
--- b/src/backend/parser/parse_agg.c
*************** transformAggregateCall(ParseState *pstat
*** 333,338 ****
--- 333,341 ----
case EXPR_KIND_TRIGGER_WHEN:
err = _("aggregate functions are not allowed in trigger WHEN conditions");
break;
+ case EXPR_KIND_ROW_SECURITY:
+ err = _("aggregate functions are not allowed in row-security policy");
+ break;
/*
* There is intentionally no default: case here, so that the
*************** transformWindowFuncCall(ParseState *psta
*** 662,667 ****
--- 665,673 ----
case EXPR_KIND_TRIGGER_WHEN:
err = _("window functions are not allowed in trigger WHEN conditions");
break;
+ case EXPR_KIND_ROW_SECURITY:
+ err = _("window functions are not allowed in row-security policy");
+ break;
/*
* There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
new file mode 100644
index 81c9338..e289e77 100644
*** a/src/backend/parser/parse_expr.c
--- b/src/backend/parser/parse_expr.c
*************** transformSubLink(ParseState *pstate, Sub
*** 1462,1467 ****
--- 1462,1468 ----
case EXPR_KIND_OFFSET:
case EXPR_KIND_RETURNING:
case EXPR_KIND_VALUES:
+ case EXPR_KIND_ROW_SECURITY:
/* okay */
break;
case EXPR_KIND_CHECK_CONSTRAINT:
*************** ParseExprKindName(ParseExprKind exprKind
*** 2643,2648 ****
--- 2644,2651 ----
return "EXECUTE";
case EXPR_KIND_TRIGGER_WHEN:
return "WHEN";
+ case EXPR_KIND_ROW_SECURITY:
+ return "ROW SECURITY";
/*
* There is intentionally no default: case here, so that the
diff --git a/src/backend/rewrite/Makefile b/src/backend/rewrite/Makefile
new file mode 100644
index 9ff56c7..25423d3 100644
*** a/src/backend/rewrite/Makefile
--- b/src/backend/rewrite/Makefile
*************** top_builddir = ../../..
*** 13,18 ****
include $(top_builddir)/src/Makefile.global
OBJS = rewriteRemove.o rewriteDefine.o \
! rewriteHandler.o rewriteManip.o rewriteSupport.o
include $(top_srcdir)/src/backend/common.mk
--- 13,19 ----
include $(top_builddir)/src/Makefile.global
OBJS = rewriteRemove.o rewriteDefine.o \
! rewriteHandler.o rewriteManip.o rewriteSupport.o \
! rowsecurity.o
include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
new file mode 100644
index caed8ca..8d987a6 100644
*** a/src/backend/rewrite/rewriteHandler.c
--- b/src/backend/rewrite/rewriteHandler.c
***************
*** 25,41 ****
#include "rewrite/rewriteDefine.h"
#include "rewrite/rewriteHandler.h"
#include "rewrite/rewriteManip.h"
#include "utils/builtins.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
-
/* We use a list of these to detect recursion in RewriteQuery */
typedef struct rewrite_event
{
! Oid relation; /* OID of relation having rules */
! CmdType event; /* type of rule being fired */
} rewrite_event;
typedef struct acquireLocksOnSubLinks_context
{
--- 25,42 ----
#include "rewrite/rewriteDefine.h"
#include "rewrite/rewriteHandler.h"
#include "rewrite/rewriteManip.h"
+ #include "rewrite/rowsecurity.h"
#include "utils/builtins.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
/* We use a list of these to detect recursion in RewriteQuery */
typedef struct rewrite_event
{
! Oid relation; /* OID of relation having rules */
! CmdType event; /* type of rule being fired */
} rewrite_event;
+
typedef struct acquireLocksOnSubLinks_context
{
*************** fireRIRrules(Query *parsetree, List *act
*** 1670,1715 ****
* Collect the RIR rules that we must apply
*/
rules = rel->rd_rules;
! if (rules == NULL)
! {
! heap_close(rel, NoLock);
! continue;
! }
! locks = NIL;
! for (i = 0; i < rules->numLocks; i++)
{
! rule = rules->rules[i];
! if (rule->event != CMD_SELECT)
! continue;
! locks = lappend(locks, rule);
! }
/*
! * If we found any, apply them --- but first check for recursion!
*/
! if (locks != NIL)
{
! ListCell *l;
!
if (list_member_oid(activeRIRs, RelationGetRelid(rel)))
ereport(ERROR,
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
! errmsg("infinite recursion detected in rules for relation \"%s\"",
RelationGetRelationName(rel))));
activeRIRs = lcons_oid(RelationGetRelid(rel), activeRIRs);
!
! foreach(l, locks)
! {
! rule = lfirst(l);
!
! parsetree = ApplyRetrieveRule(parsetree,
! rule,
! rt_index,
! rel,
! activeRIRs,
! forUpdatePushedDown);
! }
activeRIRs = list_delete_first(activeRIRs);
}
--- 1671,1733 ----
* Collect the RIR rules that we must apply
*/
rules = rel->rd_rules;
! if (rules != NULL)
{
! locks = NIL;
! for (i = 0; i < rules->numLocks; i++)
! {
! rule = rules->rules[i];
! if (rule->event != CMD_SELECT)
! continue;
! locks = lappend(locks, rule);
! }
+ /*
+ * If we found any, apply them --- but first check for recursion!
+ */
+ if (locks != NIL)
+ {
+ ListCell *l;
+
+ if (list_member_oid(activeRIRs, RelationGetRelid(rel)))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("infinite recursion detected in rules for relation \"%s\"",
+ RelationGetRelationName(rel))));
+ activeRIRs = lcons_oid(RelationGetRelid(rel), activeRIRs);
+
+ foreach(l, locks)
+ {
+ rule = lfirst(l);
+
+ parsetree = ApplyRetrieveRule(parsetree,
+ rule,
+ rt_index,
+ rel,
+ activeRIRs,
+ forUpdatePushedDown);
+ }
+
+ activeRIRs = list_delete_first(activeRIRs);
+ }
+ }
/*
! * If the RTE has row-security quals, apply them and recurse into the
! * securityQuals.
*/
! if (prepend_row_security_quals(parsetree, rte, rt_index))
{
! // We applied security quals, check for infinite recursion and
! // then expand any nested queries.
if (list_member_oid(activeRIRs, RelationGetRelid(rel)))
ereport(ERROR,
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
! errmsg("infinite recursion detected in row-security policy for relation \"%s\"",
RelationGetRelationName(rel))));
activeRIRs = lcons_oid(RelationGetRelid(rel), activeRIRs);
!
! expression_tree_walker( (Node*) rte->securityQuals, fireRIRonSubLink, (void*)activeRIRs );
activeRIRs = list_delete_first(activeRIRs);
}
*************** rewriteTargetView(Query *parsetree, Rela
*** 2902,2908 ****
* rewrite_events is a list of open query-rewrite actions, so we can detect
* infinite recursion.
*/
! static List *
RewriteQuery(Query *parsetree, List *rewrite_events)
{
CmdType event = parsetree->commandType;
--- 2920,2926 ----
* rewrite_events is a list of open query-rewrite actions, so we can detect
* infinite recursion.
*/
! List *
RewriteQuery(Query *parsetree, List *rewrite_events)
{
CmdType event = parsetree->commandType;
*************** RewriteQuery(Query *parsetree, List *rew
*** 2975,2980 ****
--- 2993,2999 ----
}
}
+
/*
* If the statement is an insert, update, or delete, adjust its targetlist
* as needed, and then fire INSERT/UPDATE/DELETE rules on it.
diff --git a/src/backend/rewrite/rowsecurity.c b/src/backend/rewrite/rowsecurity.c
new file mode 100644
index ...2c2f6a3 .
*** a/src/backend/rewrite/rowsecurity.c
--- b/src/backend/rewrite/rowsecurity.c
***************
*** 0 ****
--- 1,143 ----
+ /*
+ * rewrite/rowsecurity.c
+ * Routines to support row-security feature
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ */
+ #include "postgres.h"
+
+ #include "access/heapam.h"
+ #include "access/htup_details.h"
+ #include "access/sysattr.h"
+ #include "catalog/pg_class.h"
+ #include "catalog/pg_inherits_fn.h"
+ #include "catalog/pg_rowsecurity.h"
+ #include "catalog/pg_type.h"
+ #include "miscadmin.h"
+ #include "nodes/makefuncs.h"
+ #include "nodes/nodeFuncs.h"
+ #include "nodes/plannodes.h"
+ #include "parser/parsetree.h"
+ #include "rewrite/rewriteHandler.h"
+ #include "rewrite/rowsecurity.h"
+ #include "utils/lsyscache.h"
+ #include "utils/rel.h"
+ #include "utils/syscache.h"
+ #include "tcop/utility.h"
+
+ /* hook to allow extensions to apply their own security policy */
+ row_security_policy_hook_type row_security_policy_hook = NULL;
+
+ /*
+ * Check the given RTE to see whether it's already had row-security quals
+ * expanded and, if not, prepend any row-security rules from built-in or
+ * plug-in sources to the securityQuals. The security quals are rewritten (for
+ * view expansion, etc) before being added to the RTE.
+ *
+ * Returns true if any quals were added. Note that quals may have been found
+ * but not added if user rights make the user exempt from row security.
+ */
+ bool
+ prepend_row_security_quals(Query* root, RangeTblEntry* rte, int rt_index)
+ {
+ List *rowsecquals;
+ Relation rel;
+ Oid userid;
+ int sec_context;
+ bool qualsAdded = false;
+ bool depends_on_userid;
+
+ GetUserIdAndSecContext(&userid, &sec_context);
+
+ if (rte->relid >= FirstNormalObjectId
+ && rte->relkind == 'r'
+ && !(sec_context & SECURITY_ROW_LEVEL_DISABLED))
+ {
+ /*
+ * Fetch the row-security qual and add it to the list of quals
+ * to be expanded by expand_security_quals.
+ */
+ rel = heap_open(rte->relid, NoLock);
+ rowsecquals = pull_row_security_policy(root->commandType, rel, &depends_on_userid);
+ if (rowsecquals)
+ {
+ /*
+ * Row security quals always have the target table as varno 1, as no
+ * joins are permitted in row security expressions. We must walk
+ * the expression, updating any references to varno 1 to the varno
+ * the table has in the outer query.
+ *
+ * We rewrite the expression in-place.
+ */
+ qualsAdded = true;
+ ChangeVarNodes(rowsecquals, 1, rt_index, 0);
+ rte->securityQuals = list_concat(rowsecquals, rte->securityQuals);
+ }
+ heap_close(rel, NoLock);
+ }
+ if (depends_on_userid)
+ {
+ /*
+ * Record that this plan depends on the current user ID
+ * and must be replanned if it changes.
+ */
+ Assert( !OidIsValid(root->dependsUserId) || root->dependsUserId == GetUserId() );
+ root->dependsUserId = GetUserId();
+ }
+ return qualsAdded;
+ }
+
+ /*
+ * pull_row_security_policy
+ *
+ * Fetches the configured row-security policy of both built-in catalogs and any
+ * extensions. If any policy is found then a list of qualifier expressions is
+ * returned, where each is treated as a securityQual.
+ *
+ * Vars must use varno 1 to refer to the table with row security.
+ *
+ * The returned expression trees will be modified in-place, so return copies if
+ * you're not generating the expression tree each time.
+ */
+ List *
+ pull_row_security_policy(CmdType cmd, Relation relation, bool* depends_on_userid)
+ {
+ List *quals = NIL;
+ Expr *qual = NULL;
+
+ /*
+ * Pull the row-security policy configured with built-in features,
+ * if unprivileged users. Note: superuser bypasses RLS.
+ */
+ if (relation->rsdesc)
+ {
+ if (depends_on_userid)
+ *depends_on_userid = true;
+ /* Should make this dependent on a grantable right instead: */
+ if (!superuser())
+ {
+ RowSecurityDesc *rsdesc = relation->rsdesc;
+ qual = copyObject(rsdesc->rsall.qual);
+ quals = lcons(qual, quals);
+ }
+ } else {
+ if (depends_on_userid)
+ *depends_on_userid = false;
+ }
+
+ /*
+ * Also, ask extensions whether they want to apply their own
+ * row-security policy. If both built-in and extension has
+ * their own policy they're applied as nested qualifiers.
+ */
+ if (row_security_policy_hook)
+ {
+ List *temp;
+
+ temp = (*row_security_policy_hook)(cmd, relation);
+ if (temp != NIL)
+ lcons(temp, quals);
+ }
+ return quals;
+ }
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
new file mode 100644
index 1e1e616..5630663 100644
*** a/src/backend/utils/adt/ri_triggers.c
--- b/src/backend/utils/adt/ri_triggers.c
*************** ri_PlanCheck(const char *querystr, int n
*** 2955,2960 ****
--- 2955,2961 ----
Relation query_rel;
Oid save_userid;
int save_sec_context;
+ int temp_sec_context;
/*
* Use the query type code to determine whether the query is run against
*************** ri_PlanCheck(const char *querystr, int n
*** 2967,2974 ****
/* Switch to proper UID to perform check as */
GetUserIdAndSecContext(&save_userid, &save_sec_context);
SetUserIdAndSecContext(RelationGetForm(query_rel)->relowner,
! save_sec_context | SECURITY_LOCAL_USERID_CHANGE);
/* Create the plan */
qplan = SPI_prepare(querystr, nargs, argtypes);
--- 2968,2985 ----
/* Switch to proper UID to perform check as */
GetUserIdAndSecContext(&save_userid, &save_sec_context);
+
+ /*
+ * Row-level security should be disabled in the case where a foreign-key
+ * relation is queried to check existence of tuples that references the
+ * primary-key being modified.
+ */
+ temp_sec_context = save_sec_context | SECURITY_LOCAL_USERID_CHANGE;
+ if (qkey->constr_queryno != RI_PLAN_CHECK_LOOKUPPK)
+ temp_sec_context |= SECURITY_ROW_LEVEL_DISABLED;
+
SetUserIdAndSecContext(RelationGetForm(query_rel)->relowner,
! temp_sec_context);
/* Create the plan */
qplan = SPI_prepare(querystr, nargs, argtypes);
*************** ri_PerformCheck(const RI_ConstraintInfo
*** 3087,3092 ****
--- 3098,3104 ----
/* Switch to proper UID to perform check as */
GetUserIdAndSecContext(&save_userid, &save_sec_context);
+
SetUserIdAndSecContext(RelationGetForm(query_rel)->relowner,
save_sec_context | SECURITY_LOCAL_USERID_CHANGE);
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
new file mode 100644
index d492cbb..e53a6ed 100644
*** a/src/backend/utils/cache/plancache.c
--- b/src/backend/utils/cache/plancache.c
***************
*** 53,58 ****
--- 53,59 ----
#include "catalog/namespace.h"
#include "executor/executor.h"
#include "executor/spi.h"
+ #include "miscadmin.h"
#include "nodes/nodeFuncs.h"
#include "optimizer/cost.h"
#include "optimizer/planmain.h"
*************** CompleteCachedPlan(CachedPlanSource *pla
*** 371,377 ****
*/
extract_query_dependencies((Node *) querytree_list,
&plansource->relationOids,
! &plansource->invalItems);
/*
* Also save the current search_path in the query_context. (This
--- 372,379 ----
*/
extract_query_dependencies((Node *) querytree_list,
&plansource->relationOids,
! &plansource->invalItems,
! &plansource->planUserId);
/*
* Also save the current search_path in the query_context. (This
*************** RevalidateCachedQuery(CachedPlanSource *
*** 583,588 ****
--- 585,600 ----
}
/*
+ * If the plan was constructed with assumption of a particular user-id,
+ * and the current user ID is different, invalidate the plan, then rerun
+ * rewrite on the original parsetree.
+ */
+ if (plansource->is_valid &&
+ OidIsValid(plansource->planUserId) &&
+ plansource->planUserId != GetUserId())
+ plansource->is_valid = false;
+
+ /*
* If the query is currently valid, acquire locks on the referenced
* objects; then check again. We need to do it this way to cover the race
* condition that an invalidation message arrives before we get the locks.
*************** RevalidateCachedQuery(CachedPlanSource *
*** 723,729 ****
*/
extract_query_dependencies((Node *) qlist,
&plansource->relationOids,
! &plansource->invalItems);
/*
* Also save the current search_path in the query_context. (This should
--- 735,743 ----
*/
extract_query_dependencies((Node *) qlist,
&plansource->relationOids,
! &plansource->invalItems,
! &plansource->planUserId
! );
/*
* Also save the current search_path in the query_context. (This should
*************** BuildCachedPlan(CachedPlanSource *planso
*** 847,852 ****
--- 861,867 ----
{
CachedPlan *plan;
List *plist;
+ ListCell *cell;
bool snapshot_set;
bool spi_pushed;
MemoryContext plan_context;
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
new file mode 100644
index c8cea02..66e3d0c 100644
*** a/src/backend/utils/cache/relcache.c
--- b/src/backend/utils/cache/relcache.c
***************
*** 50,55 ****
--- 50,56 ----
#include "catalog/pg_opclass.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_rewrite.h"
+ #include "catalog/pg_rowsecurity.h"
#include "catalog/pg_tablespace.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
*************** RelationBuildDesc(Oid targetRelId, bool
*** 954,959 ****
--- 955,965 ----
else
relation->trigdesc = NULL;
+ if (relation->rd_rel->relhasrowsecurity)
+ RelationBuildRowSecurity(relation);
+ else
+ relation->rsdesc = NULL;
+
/*
* if it's an index, initialize index-related information
*/
*************** RelationDestroyRelation(Relation relatio
*** 1914,1919 ****
--- 1920,1927 ----
MemoryContextDelete(relation->rd_indexcxt);
if (relation->rd_rulescxt)
MemoryContextDelete(relation->rd_rulescxt);
+ if (relation->rsdesc)
+ MemoryContextDelete(relation->rsdesc->rscxt);
if (relation->rd_fdwroutine)
pfree(relation->rd_fdwroutine);
pfree(relation);
*************** RelationCacheInitializePhase3(void)
*** 3302,3308 ****
relation->rd_rel->relhastriggers = false;
restart = true;
}
!
/* Release hold on the relation */
RelationDecrementReferenceCount(relation);
--- 3310,3322 ----
relation->rd_rel->relhastriggers = false;
restart = true;
}
! if (relation->rd_rel->relhasrowsecurity && relation->rsdesc == NULL)
! {
! RelationBuildRowSecurity(relation);
! if (relation->rsdesc == NULL)
! relation->rd_rel->relhasrowsecurity = false;
! restart = true;
! }
/* Release hold on the relation */
RelationDecrementReferenceCount(relation);
*************** load_relcache_init_file(bool shared)
*** 4614,4619 ****
--- 4628,4634 ----
rel->rd_rules = NULL;
rel->rd_rulescxt = NULL;
rel->trigdesc = NULL;
+ rel->rsdesc = NULL;
rel->rd_indexprs = NIL;
rel->rd_indpred = NIL;
rel->rd_exclops = NULL;
diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c
new file mode 100644
index 4d35ae5..fd9e6cd 100644
*** a/src/bin/pg_dump/common.c
--- b/src/bin/pg_dump/common.c
*************** getSchemaData(Archive *fout, int *numTab
*** 244,249 ****
--- 244,253 ----
write_msg(NULL, "reading rewrite rules\n");
getRules(fout, &numRules);
+ if (g_verbose)
+ write_msg(NULL, "reading row-security policies\n");
+ getRowSecurity(fout, tblinfo, numTables);
+
*numTablesPtr = numTables;
return tblinfo;
}
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
new file mode 100644
index 9464540..fb4d965 100644
*** a/src/bin/pg_dump/pg_backup_archiver.c
--- b/src/bin/pg_dump/pg_backup_archiver.c
*************** _printTocEntry(ArchiveHandle *AH, TocEnt
*** 3223,3228 ****
--- 3223,3229 ----
strcmp(te->desc, "INDEX") == 0 ||
strcmp(te->desc, "RULE") == 0 ||
strcmp(te->desc, "TRIGGER") == 0 ||
+ strcmp(te->desc, "ROW SECURITY") == 0 ||
strcmp(te->desc, "USER MAPPING") == 0)
{
/* these object types don't have separate owners */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
new file mode 100644
index a6c0428..fe181ef 100644
*** a/src/bin/pg_dump/pg_dump.c
--- b/src/bin/pg_dump/pg_dump.c
*************** static char *myFormatType(const char *ty
*** 247,252 ****
--- 247,253 ----
static void getBlobs(Archive *fout);
static void dumpBlob(Archive *fout, BlobInfo *binfo);
static int dumpBlobs(Archive *fout, void *arg);
+ static void dumpRowSecurity(Archive *fout, RowSecurityInfo *rsinfo);
static void dumpDatabase(Archive *AH);
static void dumpEncoding(Archive *AH);
static void dumpStdStrings(Archive *AH);
*************** dumpBlobs(Archive *fout, void *arg)
*** 2723,2728 ****
--- 2724,2857 ----
return 1;
}
+ /*
+ * getRowSecurity
+ * get information about every row-security policy on a dumpable table
+ */
+ void
+ getRowSecurity(Archive *fout, TableInfo tblinfo[], int numTables)
+ {
+ PQExpBuffer query = createPQExpBuffer();
+ PGresult *res;
+ RowSecurityInfo *rsinfo;
+ int i_oid;
+ int i_tableoid;
+ int i_rseccmd;
+ int i_rsecqual;
+ int i, j, ntups;
+
+ /* row-security is not supported prior to v9.4 */
+ if (fout->remoteVersion < 90400)
+ return;
+
+ for (i=0; i < numTables; i++)
+ {
+ TableInfo *tbinfo = &tblinfo[i];
+
+ if (!tbinfo->hasrowsec || !tbinfo->dobj.dump)
+ continue;
+
+ if (g_verbose)
+ write_msg(NULL, "reading row-security policy for table \"%s\"\n",
+ tbinfo->dobj.name);
+
+ /*
+ * select table schema to ensure regproc name is qualified if needed
+ */
+ selectSourceSchema(fout, tbinfo->dobj.namespace->dobj.name);
+
+ resetPQExpBuffer(query);
+
+ appendPQExpBuffer(query,
+ "SELECT oid, tableoid, s.rseccmd, "
+ "pg_get_expr(s.rsecqual, s.rsecrelid) AS rsecqual "
+ "FROM pg_catalog.pg_rowsecurity s "
+ "WHERE rsecrelid = '%u'",
+ tbinfo->dobj.catId.oid);
+ res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+
+ ntups = PQntuples(res);
+
+ i_oid = PQfnumber(res, "oid");
+ i_tableoid = PQfnumber(res, "tableoid");
+ i_rseccmd = PQfnumber(res, "rseccmd");
+ i_rsecqual = PQfnumber(res, "rsecqual");
+
+ rsinfo = pg_malloc(ntups * sizeof(RowSecurityInfo));
+ for (j=0; j < ntups; j++)
+ {
+ char namebuf[NAMEDATALEN + 1];
+
+ rsinfo[j].dobj.objType = DO_ROW_SECURITY;
+ rsinfo[j].dobj.catId.tableoid =
+ atooid(PQgetvalue(res, j, i_tableoid));
+ rsinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
+ AssignDumpId(&rsinfo[j].dobj);
+ snprintf(namebuf, sizeof(namebuf), "row-security of %s",
+ tbinfo->rolname);
+ rsinfo[j].dobj.name = namebuf;
+ rsinfo[j].dobj.namespace = tbinfo->dobj.namespace;
+ rsinfo[j].rstable = tbinfo;
+ rsinfo[j].rseccmd = pg_strdup(PQgetvalue(res, j, i_rseccmd));
+ rsinfo[j].rsecqual = pg_strdup(PQgetvalue(res, j, i_rsecqual));
+ }
+ PQclear(res);
+ }
+ destroyPQExpBuffer(query);
+ }
+
+ /*
+ * dumpRowSecurity
+ * dump the definition of the given row-security policy
+ */
+ static void
+ dumpRowSecurity(Archive *fout, RowSecurityInfo *rsinfo)
+ {
+ TableInfo *tbinfo = rsinfo->rstable;
+ PQExpBuffer query;
+ PQExpBuffer delqry;
+ const char *cmd;
+
+ if (dataOnly || !tbinfo->hasrowsec)
+ return;
+
+ query = createPQExpBuffer();
+ delqry = createPQExpBuffer();
+ appendPQExpBuffer(query, "ALTER TABLE %s SET ROW SECURITY ",
+ fmtId(tbinfo->dobj.name));
+ appendPQExpBuffer(delqry, "ALTER TABLE %s RESET ROW SECURITY ",
+ fmtId(tbinfo->dobj.name));
+ if (strcmp(rsinfo->rseccmd, "a") == 0)
+ cmd = "ALL";
+ else if (strcmp(rsinfo->rseccmd, "s") == 0)
+ cmd = "SELECT";
+ else if (strcmp(rsinfo->rseccmd, "i") == 0)
+ cmd = "INSERT";
+ else if (strcmp(rsinfo->rseccmd, "u") == 0)
+ cmd = "UPDATE";
+ else if (strcmp(rsinfo->rseccmd, "d") == 0)
+ cmd = "DELETE";
+ else
+ {
+ write_msg(NULL, "unexpected command type: '%s'\n", rsinfo->rseccmd);
+ exit_nicely(1);
+ }
+ appendPQExpBuffer(query, "FOR %s TO %s;\n", cmd, rsinfo->rsecqual);
+ appendPQExpBuffer(delqry, "FOR %s;\n", cmd);
+
+ ArchiveEntry(fout, rsinfo->dobj.catId, rsinfo->dobj.dumpId,
+ rsinfo->dobj.name,
+ rsinfo->dobj.namespace->dobj.name,
+ NULL,
+ tbinfo->rolname, false,
+ "ROW SECURITY", SECTION_POST_DATA,
+ query->data, delqry->data, NULL,
+ NULL, 0,
+ NULL, NULL);
+
+ destroyPQExpBuffer(query);
+ }
+
static void
binary_upgrade_set_type_oids_by_type_oid(Archive *fout,
PQExpBuffer upgrade_buffer,
*************** getTables(Archive *fout, int *numTables)
*** 4253,4258 ****
--- 4382,4388 ----
int i_relhastriggers;
int i_relhasindex;
int i_relhasrules;
+ int i_relhasrowsec;
int i_relhasoids;
int i_relfrozenxid;
int i_toastoid;
*************** getTables(Archive *fout, int *numTables)
*** 4304,4313 ****
"(%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, c.relispopulated, "
! "c.relreplident, c.relpages, "
"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, "
--- 4434,4440 ----
"(%s c.relowner) AS rolname, "
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
! "c.relhasrowsecurity, "
"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, "
*************** getTables(Archive *fout, int *numTables)
*** 4343,4348 ****
--- 4470,4476 ----
"(%s c.relowner) AS rolname, "
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"c.relfrozenxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"c.relpersistence, c.relispopulated, "
*************** getTables(Archive *fout, int *numTables)
*** 4382,4387 ****
--- 4510,4516 ----
"(%s c.relowner) AS rolname, "
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"c.relfrozenxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"c.relpersistence, 't' as relispopulated, "
*************** getTables(Archive *fout, int *numTables)
*** 4419,4424 ****
--- 4548,4554 ----
"(%s c.relowner) AS rolname, "
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"c.relfrozenxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"'p' AS relpersistence, 't' as relispopulated, "
*************** getTables(Archive *fout, int *numTables)
*** 4455,4460 ****
--- 4585,4591 ----
"(%s c.relowner) AS rolname, "
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"c.relfrozenxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"'p' AS relpersistence, 't' as relispopulated, "
*************** getTables(Archive *fout, int *numTables)
*** 4491,4496 ****
--- 4622,4628 ----
"(%s c.relowner) AS rolname, "
"c.relchecks, (c.reltriggers <> 0) AS relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"c.relfrozenxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"'p' AS relpersistence, 't' as relispopulated, "
*************** getTables(Archive *fout, int *numTables)
*** 4527,4532 ****
--- 4659,4665 ----
"(%s relowner) AS rolname, "
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"0 AS relfrozenxid, "
"0 AS toid, "
"0 AS tfrozenxid, "
*************** getTables(Archive *fout, int *numTables)
*** 4563,4568 ****
--- 4696,4702 ----
"(%s relowner) AS rolname, "
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"0 AS relfrozenxid, "
"0 AS toid, "
"0 AS tfrozenxid, "
*************** getTables(Archive *fout, int *numTables)
*** 4595,4600 ****
--- 4729,4735 ----
"(%s relowner) AS rolname, "
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"0 AS relfrozenxid, "
"0 AS toid, "
"0 AS tfrozenxid, "
*************** getTables(Archive *fout, int *numTables)
*** 4622,4627 ****
--- 4757,4763 ----
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, "
"'t'::bool AS relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"0 AS relfrozenxid, "
"0 AS toid, "
"0 AS tfrozenxid, "
*************** getTables(Archive *fout, int *numTables)
*** 4659,4664 ****
--- 4795,4801 ----
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, "
"'t'::bool AS relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"0 as relfrozenxid, "
"0 AS toid, "
"0 AS tfrozenxid, "
*************** getTables(Archive *fout, int *numTables)
*** 4706,4711 ****
--- 4843,4849 ----
i_relhastriggers = PQfnumber(res, "relhastriggers");
i_relhasindex = PQfnumber(res, "relhasindex");
i_relhasrules = PQfnumber(res, "relhasrules");
+ i_relhasrowsec = PQfnumber(res, "relhasrowsecurity");
i_relhasoids = PQfnumber(res, "relhasoids");
i_relfrozenxid = PQfnumber(res, "relfrozenxid");
i_toastoid = PQfnumber(res, "toid");
*************** getTables(Archive *fout, int *numTables)
*** 4755,4760 ****
--- 4893,4899 ----
tblinfo[i].hasindex = (strcmp(PQgetvalue(res, i, i_relhasindex), "t") == 0);
tblinfo[i].hasrules = (strcmp(PQgetvalue(res, i, i_relhasrules), "t") == 0);
tblinfo[i].hastriggers = (strcmp(PQgetvalue(res, i, i_relhastriggers), "t") == 0);
+ tblinfo[i].hasrowsec = (strcmp(PQgetvalue(res, i, i_relhasrowsec), "t") == 0);
tblinfo[i].hasoids = (strcmp(PQgetvalue(res, i, i_relhasoids), "t") == 0);
tblinfo[i].relispopulated = (strcmp(PQgetvalue(res, i, i_relispopulated), "t") == 0);
tblinfo[i].relreplident = *(PQgetvalue(res, i, i_relreplident));
*************** dumpDumpableObject(Archive *fout, Dumpab
*** 7878,7883 ****
--- 8017,8025 ----
NULL, 0,
dumpBlobs, NULL);
break;
+ case DO_ROW_SECURITY:
+ dumpRowSecurity(fout, (RowSecurityInfo *) dobj);
+ break;
case DO_PRE_DATA_BOUNDARY:
case DO_POST_DATA_BOUNDARY:
/* never dumped, nothing to do */
*************** addBoundaryDependencies(DumpableObject *
*** 15261,15266 ****
--- 15403,15409 ----
case DO_TRIGGER:
case DO_EVENT_TRIGGER:
case DO_DEFAULT_ACL:
+ case DO_ROW_SECURITY:
/* Post-data objects: must come after the post-data boundary */
addObjectDependency(dobj, postDataBound->dumpId);
break;
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
new file mode 100644
index e01015e..08e0fdd 100644
*** a/src/bin/pg_dump/pg_dump.h
--- b/src/bin/pg_dump/pg_dump.h
*************** typedef enum
*** 111,117 ****
DO_PRE_DATA_BOUNDARY,
DO_POST_DATA_BOUNDARY,
DO_EVENT_TRIGGER,
! DO_REFRESH_MATVIEW
} DumpableObjectType;
typedef struct _dumpableObject
--- 111,118 ----
DO_PRE_DATA_BOUNDARY,
DO_POST_DATA_BOUNDARY,
DO_EVENT_TRIGGER,
! DO_REFRESH_MATVIEW,
! DO_ROW_SECURITY,
} DumpableObjectType;
typedef struct _dumpableObject
*************** typedef struct _tableInfo
*** 245,250 ****
--- 246,252 ----
bool hasindex; /* does it have any indexes? */
bool hasrules; /* does it have any rules? */
bool hastriggers; /* does it have any triggers? */
+ bool hasrowsec; /* does it have any row-security policy? */
bool hasoids; /* does it have OIDs? */
uint32 frozenxid; /* for restore frozen xid */
Oid toast_oid; /* for restore toast frozen xid */
*************** typedef struct _blobInfo
*** 484,489 ****
--- 486,499 ----
char *blobacl;
} BlobInfo;
+ typedef struct _rowSecurityInfo
+ {
+ DumpableObject dobj;
+ TableInfo *rstable;
+ char *rseccmd;
+ char *rsecqual;
+ } RowSecurityInfo;
+
/* global decls */
extern bool force_quotes; /* double-quotes for identifiers flag */
extern bool g_verbose; /* verbose flag */
*************** extern DefaultACLInfo *getDefaultACLs(Ar
*** 575,579 ****
--- 585,590 ----
extern void getExtensionMembership(Archive *fout, ExtensionInfo extinfo[],
int numExtensions);
extern EventTriggerInfo *getEventTriggers(Archive *fout, int *numEventTriggers);
+ extern void getRowSecurity(Archive *fout, TableInfo tblinfo[], int numTables);
#endif /* PG_DUMP_H */
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
new file mode 100644
index 3590932..48602e5 100644
*** a/src/bin/pg_dump/pg_dump_sort.c
--- b/src/bin/pg_dump/pg_dump_sort.c
*************** describeDumpableObject(DumpableObject *o
*** 1398,1403 ****
--- 1398,1408 ----
"BLOB DATA (ID %d)",
obj->dumpId);
return;
+ case DO_ROW_SECURITY:
+ snprintf(buf, bufsize,
+ "ROW-SECURITY POLICY (ID %d OID %u)",
+ obj->dumpId, obj->catId.oid);
+ return;
case DO_PRE_DATA_BOUNDARY:
snprintf(buf, bufsize,
"PRE-DATA BOUNDARY (ID %d)",
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
new file mode 100644
index d1447fe..d8225ea 100644
*** a/src/bin/psql/describe.c
--- b/src/bin/psql/describe.c
*************** listTables(const char *tabtypes, const c
*** 2795,2800 ****
--- 2795,2804 ----
appendPQExpBuffer(&buf,
",\n pg_catalog.obj_description(c.oid, 'pg_class') as \"%s\"",
gettext_noop("Description"));
+ if (pset.sversion >= 90400)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_get_expr(rs.rsecqual, c.oid) as \"%s\"",
+ gettext_noop("Row-security"));
}
appendPQExpBufferStr(&buf,
*************** listTables(const char *tabtypes, const c
*** 2804,2809 ****
--- 2808,2816 ----
appendPQExpBufferStr(&buf,
"\n LEFT JOIN pg_catalog.pg_index i ON i.indexrelid = c.oid"
"\n LEFT JOIN pg_catalog.pg_class c2 ON i.indrelid = c2.oid");
+ if (verbose && pset.sversion >= 90400)
+ appendPQExpBuffer(&buf,
+ "\n LEFT JOIN pg_rowsecurity rs ON rs.rsecrelid = c.oid");
appendPQExpBufferStr(&buf, "\nWHERE c.relkind IN (");
if (showTables)
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
new file mode 100644
index 8948589..de9323e 100644
*** a/src/include/catalog/dependency.h
--- b/src/include/catalog/dependency.h
*************** typedef enum ObjectClass
*** 147,152 ****
--- 147,153 ----
OCLASS_DEFACL, /* pg_default_acl */
OCLASS_EXTENSION, /* pg_extension */
OCLASS_EVENT_TRIGGER, /* pg_event_trigger */
+ OCLASS_ROWSECURITY, /* pg_rowsecurity */
MAX_OCLASS /* MUST BE LAST */
} ObjectClass;
diff --git a/src/include/catalog/indexing.h b/src/include/catalog/indexing.h
new file mode 100644
index 0515b75..4f46c5d 100644
*** a/src/include/catalog/indexing.h
--- b/src/include/catalog/indexing.h
*************** DECLARE_UNIQUE_INDEX(pg_extension_name_i
*** 313,318 ****
--- 313,323 ----
DECLARE_UNIQUE_INDEX(pg_range_rngtypid_index, 3542, on pg_range using btree(rngtypid oid_ops));
#define RangeTypidIndexId 3542
+ DECLARE_UNIQUE_INDEX(pg_rowsecurity_oid_index, 5001, on pg_rowsecurity using btree(oid oid_ops));
+ #define RowSecurityOidIndexId 5001
+ DECLARE_UNIQUE_INDEX(pg_rowsecurity_relid_index, 5002, on pg_rowsecurity using btree(rsecrelid oid_ops, rseccmd char_ops));
+ #define RowSecurityRelidIndexId 5002
+
/* 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
new file mode 100644
index f2fb317..bbbf62d 100644
*** a/src/include/catalog/pg_class.h
--- b/src/include/catalog/pg_class.h
*************** CATALOG(pg_class,1259) BKI_BOOTSTRAP BKI
*** 65,70 ****
--- 65,71 ----
bool relhasrules; /* has (or has had) any rules */
bool relhastriggers; /* has (or has had) any TRIGGERs */
bool relhassubclass; /* has (or has had) derived classes */
+ bool relhasrowsecurity; /* has (or has had) row-security policy */
bool relispopulated; /* matview currently holds query results */
char relreplident; /* see REPLICA_IDENTITY_xxx constants */
TransactionId relfrozenxid; /* all Xids < this are frozen in this rel */
*************** typedef FormData_pg_class *Form_pg_class
*** 94,100 ****
* ----------------
*/
! #define Natts_pg_class 29
#define Anum_pg_class_relname 1
#define Anum_pg_class_relnamespace 2
#define Anum_pg_class_reltype 3
--- 95,101 ----
* ----------------
*/
! #define Natts_pg_class 30
#define Anum_pg_class_relname 1
#define Anum_pg_class_relnamespace 2
#define Anum_pg_class_reltype 3
*************** typedef FormData_pg_class *Form_pg_class
*** 118,129 ****
#define Anum_pg_class_relhasrules 21
#define Anum_pg_class_relhastriggers 22
#define Anum_pg_class_relhassubclass 23
! #define Anum_pg_class_relispopulated 24
! #define Anum_pg_class_relreplident 25
! #define Anum_pg_class_relfrozenxid 26
! #define Anum_pg_class_relminmxid 27
! #define Anum_pg_class_relacl 28
! #define Anum_pg_class_reloptions 29
/* ----------------
* initial contents of pg_class
--- 119,131 ----
#define Anum_pg_class_relhasrules 21
#define Anum_pg_class_relhastriggers 22
#define Anum_pg_class_relhassubclass 23
! #define Anum_pg_class_relhasrowsecurity 24
! #define Anum_pg_class_relispopulated 25
! #define Anum_pg_class_relreplident 26
! #define Anum_pg_class_relfrozenxid 27
! #define Anum_pg_class_relminmxid 28
! #define Anum_pg_class_relacl 29
! #define Anum_pg_class_reloptions 30
/* ----------------
* initial contents of pg_class
*************** typedef FormData_pg_class *Form_pg_class
*** 138,150 ****
* Note: "3" in the relfrozenxid column stands for FirstNormalTransactionId;
* similarly, "1" in relminmxid stands for FirstMultiXactId
*/
! DATA(insert OID = 1247 ( pg_type PGNSP 71 0 PGUID 0 0 0 0 0 0 0 f f p r 30 0 t f f f f t n 3 1 _null_ _null_ ));
DESCR("");
! DATA(insert OID = 1249 ( pg_attribute PGNSP 75 0 PGUID 0 0 0 0 0 0 0 f f p r 21 0 f f f f f t n 3 1 _null_ _null_ ));
DESCR("");
! DATA(insert OID = 1255 ( pg_proc PGNSP 81 0 PGUID 0 0 0 0 0 0 0 f f p r 27 0 t f f f f t n 3 1 _null_ _null_ ));
DESCR("");
! DATA(insert OID = 1259 ( pg_class PGNSP 83 0 PGUID 0 0 0 0 0 0 0 f f p r 29 0 t f f f f t n 3 1 _null_ _null_ ));
DESCR("");
--- 140,153 ----
* Note: "3" in the relfrozenxid column stands for FirstNormalTransactionId;
* similarly, "1" in relminmxid stands for FirstMultiXactId
*/
!
! DATA(insert OID = 1247 ( pg_type PGNSP 71 0 PGUID 0 0 0 0 0 0 0 f f p r 30 0 t f f f f f t n 3 1 _null_ _null_ ));
DESCR("");
! DATA(insert OID = 1249 ( pg_attribute PGNSP 75 0 PGUID 0 0 0 0 0 0 0 f f p r 21 0 f f f f f f t n 3 1 _null_ _null_ ));
DESCR("");
! DATA(insert OID = 1255 ( pg_proc PGNSP 81 0 PGUID 0 0 0 0 0 0 0 f f p r 27 0 t f f f f f t n 3 1 _null_ _null_ ));
DESCR("");
! DATA(insert OID = 1259 ( pg_class PGNSP 83 0 PGUID 0 0 0 0 0 0 0 f f p r 30 0 t f f f f f t n 3 1 _null_ _null_ ));
DESCR("");
diff --git a/src/include/catalog/pg_rowsecurity.h b/src/include/catalog/pg_rowsecurity.h
new file mode 100644
index ...f1158d7 .
*** a/src/include/catalog/pg_rowsecurity.h
--- b/src/include/catalog/pg_rowsecurity.h
***************
*** 0 ****
--- 1,76 ----
+ /*
+ * pg_rowsecurity.h
+ * definition of the system catalog for row-security policy (pg_rowsecurity)
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ */
+ #ifndef PG_ROWSECURITY_H
+ #define PG_ROWSECURITY_H
+
+ #include "catalog/genbki.h"
+ #include "nodes/primnodes.h"
+ #include "utils/memutils.h"
+ #include "utils/relcache.h"
+
+ /* ----------------
+ * pg_rowsecurity definition. cpp turns this into
+ * typedef struct FormData_pg_rowsecurity
+ * ----------------
+ */
+ #define RowSecurityRelationId 5000
+
+ CATALOG(pg_rowsecurity,5000)
+ {
+ /* Oid of the relation that has row-security policy */
+ Oid rsecrelid;
+
+ /* One of ROWSECURITY_CMD_* below */
+ char rseccmd;
+ #ifdef CATALOG_VARLEN
+ pg_node_tree rsecqual;
+ #endif
+ } FormData_pg_rowsecurity;
+
+ /* ----------------
+ * Form_pg_rowsecurity corresponds to a pointer to a row with
+ * the format of pg_rowsecurity relation.
+ * ----------------
+ */
+ typedef FormData_pg_rowsecurity *Form_pg_rowsecurity;
+
+ /* ----------------
+ * compiler constants for pg_rowsecurity
+ * ----------------
+ */
+ #define Natts_pg_rowsecurity 3
+ #define Anum_pg_rowsecurity_rsecrelid 1
+ #define Anum_pg_rowsecurity_rseccmd 2
+ #define Anum_pg_rowsecurity_rsecqual 3
+
+ #define ROWSECURITY_CMD_ALL 'a'
+ #define ROWSECURITY_CMD_SELECT 's'
+ #define ROWSECURITY_CMD_INSERT 'i'
+ #define ROWSECURITY_CMD_UPDATE 'u'
+ #define ROWSECURITY_CMD_DELETE 'd'
+
+ typedef struct
+ {
+ Oid rsecid;
+ Expr *qual;
+ bool hassublinks;
+ } RowSecurityEntry;
+
+ typedef struct
+ {
+ MemoryContext rscxt;
+ RowSecurityEntry rsall; /* row-security policy for ALL */
+ } RowSecurityDesc;
+
+ extern void RelationBuildRowSecurity(Relation relation);
+ extern void ATExecSetRowSecurity(Relation relation,
+ const char *cmdname, Node *clause);
+ extern void RemoveRowSecurityById(Oid relationId);
+
+ #endif /* PG_ROWSECURITY_H */
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
new file mode 100644
index 0d61b82..dae01ad 100644
*** a/src/include/miscadmin.h
--- b/src/include/miscadmin.h
*************** extern int trace_recovery(int trace_leve
*** 272,277 ****
--- 272,278 ----
/* flags to be OR'd to form sec_context */
#define SECURITY_LOCAL_USERID_CHANGE 0x0001
#define SECURITY_RESTRICTED_OPERATION 0x0002
+ #define SECURITY_ROW_LEVEL_DISABLED 0x0004
extern char *DatabasePath;
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
new file mode 100644
index 18d4991..ad867fe 100644
*** a/src/include/nodes/parsenodes.h
--- b/src/include/nodes/parsenodes.h
*************** typedef struct Query
*** 152,157 ****
--- 152,161 ----
List *constraintDeps; /* a list of pg_constraint OIDs that the query
* depends on to be semantically valid */
+
+ Oid dependsUserId; /* If this rewritten tree is only valid
+ * for a given user ID, that ID. Otherwise InvalidOid.
+ * Not used after PlannerGlobal is created. */
} Query;
*************** typedef enum AlterTableType
*** 1317,1322 ****
--- 1321,1328 ----
AT_AddOf, /* OF */
AT_DropOf, /* NOT OF */
AT_ReplicaIdentity, /* REPLICA IDENTITY */
+ AT_SetRowSecurity, /* SET ROW SECURITY (...) */
+ AT_ResetRowSecurity, /* RESET ROW SECURITY */
AT_GenericOptions /* OPTIONS (...) */
} AlterTableType;
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
new file mode 100644
index 38c039c..35c88cc 100644
*** a/src/include/nodes/plannodes.h
--- b/src/include/nodes/plannodes.h
*************** typedef struct PlannedStmt
*** 67,72 ****
--- 67,74 ----
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
new file mode 100644
index c607b36..c464db0 100644
*** a/src/include/nodes/relation.h
--- b/src/include/nodes/relation.h
*************** typedef struct PlannerGlobal
*** 99,104 ****
--- 99,107 ----
Index lastRowMarkId; /* highest PlanRowMark ID assigned */
bool transientPlan; /* redo plan when TransactionXmin changes? */
+
+ Oid planUserId; /* User-id this plan depends on, or
+ * InvalidOid if it doesn't depend on a userid. */
} PlannerGlobal;
/* macro for fetching the Plan associated with a SubPlan node */
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
new file mode 100644
index 8bdb7db..193b1de 100644
*** a/src/include/optimizer/planmain.h
--- b/src/include/optimizer/planmain.h
*************** extern void set_sa_opfuncid(ScalarArrayO
*** 133,138 ****
extern void record_plan_function_dependency(PlannerInfo *root, Oid funcid);
extern void extract_query_dependencies(Node *query,
List **relationOids,
! List **invalItems);
#endif /* PLANMAIN_H */
--- 133,139 ----
extern void record_plan_function_dependency(PlannerInfo *root, Oid funcid);
extern void extract_query_dependencies(Node *query,
List **relationOids,
! List **invalItems,
! Oid *planUserId);
#endif /* PLANMAIN_H */
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
new file mode 100644
index 85598e8..9c7c382 100644
*** a/src/include/parser/parse_node.h
--- b/src/include/parser/parse_node.h
*************** typedef enum ParseExprKind
*** 63,69 ****
EXPR_KIND_INDEX_PREDICATE, /* index predicate */
EXPR_KIND_ALTER_COL_TRANSFORM, /* transform expr in ALTER COLUMN TYPE */
EXPR_KIND_EXECUTE_PARAMETER, /* parameter value in EXECUTE */
! EXPR_KIND_TRIGGER_WHEN /* WHEN condition in CREATE TRIGGER */
} ParseExprKind;
--- 63,70 ----
EXPR_KIND_INDEX_PREDICATE, /* index predicate */
EXPR_KIND_ALTER_COL_TRANSFORM, /* transform expr in ALTER COLUMN TYPE */
EXPR_KIND_EXECUTE_PARAMETER, /* parameter value in EXECUTE */
! EXPR_KIND_TRIGGER_WHEN, /* WHEN condition in CREATE TRIGGER */
! EXPR_KIND_ROW_SECURITY /* ROW SECURITY policy for a table */
} ParseExprKind;
diff --git a/src/include/rewrite/rowsecurity.h b/src/include/rewrite/rowsecurity.h
new file mode 100644
index ...5b3f0c3 .
*** a/src/include/rewrite/rowsecurity.h
--- b/src/include/rewrite/rowsecurity.h
***************
*** 0 ****
--- 1,27 ----
+ /* -------------------------------------------------------------------------
+ *
+ * rowsecurity.h
+ * prototypes for optimizer/rowsecurity.c
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * -------------------------------------------------------------------------
+ */
+ #ifndef ROWSECURITY_H
+ #define ROWSECURITY_H
+
+ #include "nodes/execnodes.h"
+ #include "nodes/parsenodes.h"
+ #include "nodes/relation.h"
+ #include "utils/rel.h"
+
+ typedef List *(*row_security_policy_hook_type)(CmdType cmdtype,
+ Relation relation);
+ extern PGDLLIMPORT row_security_policy_hook_type row_security_policy_hook;
+
+ extern List *pull_row_security_policy(CmdType cmd, Relation relation, bool *depends_on_userid);
+
+ extern bool prepend_row_security_quals(Query* root, RangeTblEntry* rte, int rt_index);
+
+ #endif /* ROWSECURITY_H */
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
new file mode 100644
index b8ca643..00f6b66 100644
*** a/src/include/utils/plancache.h
--- b/src/include/utils/plancache.h
*************** typedef struct CachedPlanSource
*** 93,98 ****
--- 93,101 ----
List *invalItems; /* other dependencies, as PlanInvalItems */
struct OverrideSearchPath *search_path; /* search_path used for
* parsing and planning */
+ Oid planUserId; /* User-id that the plan depends on, or
+ * InvalidOid if the plan doesn't depend on any
+ * particular user ID. */
MemoryContext query_context; /* context holding the above, or NULL */
/* If we have a generic plan, this is a reference-counted link to it: */
struct CachedPlan *gplan; /* generic plan, or NULL if not valid */
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
new file mode 100644
index c87dadc..3fc3e5e 100644
*** a/src/include/utils/rel.h
--- b/src/include/utils/rel.h
***************
*** 18,23 ****
--- 18,24 ----
#include "catalog/pg_am.h"
#include "catalog/pg_class.h"
#include "catalog/pg_index.h"
+ #include "catalog/pg_rowsecurity.h"
#include "fmgr.h"
#include "nodes/bitmapset.h"
#include "rewrite/prs2lock.h"
*************** typedef struct RelationData
*** 110,115 ****
--- 111,117 ----
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 */
+ RowSecurityDesc *rsdesc; /* Row-security policy, or NULL */
/*
* The index chosen as the relation's replication identity or
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
new file mode 100644
index ...0f456ea .
*** a/src/test/regress/expected/rowsecurity.out
--- b/src/test/regress/expected/rowsecurity.out
***************
*** 0 ****
--- 1,991 ----
+ --
+ -- Test of Row-level security feature
+ --
+ -- Clean up in case a prior regression run failed
+ -- Suppress NOTICE messages when users/groups don't exist
+ SET client_min_messages TO 'warning';
+ DROP USER IF EXISTS rls_regress_user0;
+ DROP USER IF EXISTS rls_regress_user1;
+ DROP USER IF EXISTS rls_regress_user2;
+ DROP SCHEMA IF EXISTS rls_regress_schema CASCADE;
+ RESET client_min_messages;
+ -- initial setup
+ CREATE USER rls_regress_user0;
+ CREATE USER rls_regress_user1;
+ CREATE USER rls_regress_user2;
+ CREATE SCHEMA rls_regress_schema;
+ GRANT ALL ON SCHEMA rls_regress_schema TO public;
+ SET search_path = rls_regress_schema;
+ -- setup of malicious function
+ CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool
+ COST 0.0000001 LANGUAGE plpgsql
+ AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END';
+ GRANT EXECUTE ON FUNCTION f_leak(text) TO public;
+ -- BASIC Row-Level Security Scenario
+ SET SESSION AUTHORIZATION rls_regress_user0;
+ CREATE TABLE uaccount (
+ pguser name primary key,
+ seclv int
+ );
+ INSERT INTO uaccount VALUES
+ ('rls_regress_user0', 99),
+ ('rls_regress_user1', 1),
+ ('rls_regress_user2', 2),
+ ('rls_regress_user3', 3);
+ GRANT SELECT ON uaccount TO public;
+ CREATE TABLE category (
+ cid int primary key,
+ cname text
+ );
+ GRANT ALL ON category TO public;
+ INSERT INTO category VALUES
+ (11, 'novel'),
+ (22, 'science fiction'),
+ (33, 'technology'),
+ (44, 'manga');
+ CREATE TABLE document (
+ did int primary key,
+ cid int references category(cid),
+ dlevel int not null,
+ dauthor name,
+ dtitle text
+ );
+ GRANT ALL ON document TO public;
+ INSERT INTO document VALUES
+ ( 1, 11, 1, 'rls_regress_user1', 'my first novel'),
+ ( 2, 11, 2, 'rls_regress_user1', 'my second novel'),
+ ( 3, 22, 2, 'rls_regress_user1', 'my science fiction'),
+ ( 4, 44, 1, 'rls_regress_user1', 'my first manga'),
+ ( 5, 44, 2, 'rls_regress_user1', 'my second manga'),
+ ( 6, 22, 1, 'rls_regress_user2', 'great science fiction'),
+ ( 7, 33, 2, 'rls_regress_user2', 'great technology book'),
+ ( 8, 44, 1, 'rls_regress_user2', 'great manga');
+ -- user's security level must higher than or equal to document's one
+ ALTER TABLE document SET ROW SECURITY FOR ALL
+ TO (dlevel <= (SELECT seclv FROM uaccount WHERE pguser = current_user));
+ -- viewpoint from rls_regress_user1
+ SET SESSION AUTHORIZATION rls_regress_user1;
+ SELECT * FROM document WHERE f_leak(dtitle) ORDER BY did;
+ NOTICE: f_leak => my first novel
+ NOTICE: f_leak => my first manga
+ NOTICE: f_leak => great science fiction
+ NOTICE: f_leak => great manga
+ did | cid | dlevel | dauthor | dtitle
+ -----+-----+--------+-------------------+-----------------------
+ 1 | 11 | 1 | rls_regress_user1 | my first novel
+ 4 | 44 | 1 | rls_regress_user1 | my first manga
+ 6 | 22 | 1 | rls_regress_user2 | great science fiction
+ 8 | 44 | 1 | rls_regress_user2 | great manga
+ (4 rows)
+
+ SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle) ORDER BY did;
+ NOTICE: f_leak => my first novel
+ NOTICE: f_leak => my first manga
+ NOTICE: f_leak => great science fiction
+ NOTICE: f_leak => great manga
+ cid | did | dlevel | dauthor | dtitle | cname
+ -----+-----+--------+-------------------+-----------------------+-----------------
+ 11 | 1 | 1 | rls_regress_user1 | my first novel | novel
+ 44 | 4 | 1 | rls_regress_user1 | my first manga | manga
+ 22 | 6 | 1 | rls_regress_user2 | great science fiction | science fiction
+ 44 | 8 | 1 | rls_regress_user2 | great manga | manga
+ (4 rows)
+
+ -- viewpoint from rls_regress_user2
+ SET SESSION AUTHORIZATION rls_regress_user2;
+ SELECT * FROM document WHERE f_leak(dtitle) ORDER BY did;
+ NOTICE: f_leak => my first novel
+ NOTICE: f_leak => my second novel
+ NOTICE: f_leak => my science fiction
+ NOTICE: f_leak => my first manga
+ NOTICE: f_leak => my second manga
+ NOTICE: f_leak => great science fiction
+ NOTICE: f_leak => great technology book
+ NOTICE: f_leak => great manga
+ did | cid | dlevel | dauthor | dtitle
+ -----+-----+--------+-------------------+-----------------------
+ 1 | 11 | 1 | rls_regress_user1 | my first novel
+ 2 | 11 | 2 | rls_regress_user1 | my second novel
+ 3 | 22 | 2 | rls_regress_user1 | my science fiction
+ 4 | 44 | 1 | rls_regress_user1 | my first manga
+ 5 | 44 | 2 | rls_regress_user1 | my second manga
+ 6 | 22 | 1 | rls_regress_user2 | great science fiction
+ 7 | 33 | 2 | rls_regress_user2 | great technology book
+ 8 | 44 | 1 | rls_regress_user2 | great manga
+ (8 rows)
+
+ SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle) ORDER BY did;
+ NOTICE: f_leak => my first novel
+ NOTICE: f_leak => my second novel
+ NOTICE: f_leak => my science fiction
+ NOTICE: f_leak => my first manga
+ NOTICE: f_leak => my second manga
+ NOTICE: f_leak => great science fiction
+ NOTICE: f_leak => great technology book
+ NOTICE: f_leak => great manga
+ cid | did | dlevel | dauthor | dtitle | cname
+ -----+-----+--------+-------------------+-----------------------+-----------------
+ 11 | 1 | 1 | rls_regress_user1 | my first novel | novel
+ 11 | 2 | 2 | rls_regress_user1 | my second novel | novel
+ 22 | 3 | 2 | rls_regress_user1 | my science fiction | science fiction
+ 44 | 4 | 1 | rls_regress_user1 | my first manga | manga
+ 44 | 5 | 2 | rls_regress_user1 | my second manga | manga
+ 22 | 6 | 1 | rls_regress_user2 | great science fiction | science fiction
+ 33 | 7 | 2 | rls_regress_user2 | great technology book | technology
+ 44 | 8 | 1 | rls_regress_user2 | great manga | manga
+ (8 rows)
+
+ EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+ QUERY PLAN
+ ----------------------------------------------------------
+ Subquery Scan on document
+ Filter: f_leak(document.dtitle)
+ -> Seq Scan on document document_1
+ Filter: (dlevel <= $0)
+ InitPlan 1 (returns $0)
+ -> Index Scan using uaccount_pkey on uaccount
+ Index Cond: (pguser = "current_user"())
+ (7 rows)
+
+ EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+ QUERY PLAN
+ ----------------------------------------------------------------------
+ Hash Join
+ Hash Cond: (category.cid = document.cid)
+ -> Seq Scan on category
+ -> Hash
+ -> Subquery Scan on document
+ Filter: f_leak(document.dtitle)
+ -> Seq Scan on document document_1
+ Filter: (dlevel <= $0)
+ InitPlan 1 (returns $0)
+ -> Index Scan using uaccount_pkey on uaccount
+ Index Cond: (pguser = "current_user"())
+ (11 rows)
+
+ -- only owner can change row-level security
+ ALTER TABLE document SET ROW SECURITY FOR ALL TO (true); -- fail
+ ERROR: must be owner of relation document
+ ALTER TABLE document RESET ROW SECURITY FOR ALL; -- fail
+ ERROR: must be owner of relation document
+ SET SESSION AUTHORIZATION rls_regress_user0;
+ ALTER TABLE document SET ROW SECURITY FOR ALL TO (dauthor = current_user);
+ -- viewpoint from rls_regress_user1 again
+ SET SESSION AUTHORIZATION rls_regress_user1;
+ SELECT * FROM document WHERE f_leak(dtitle) ORDER BY did;
+ NOTICE: f_leak => my first novel
+ NOTICE: f_leak => my second novel
+ NOTICE: f_leak => my science fiction
+ NOTICE: f_leak => my first manga
+ NOTICE: f_leak => my second manga
+ did | cid | dlevel | dauthor | dtitle
+ -----+-----+--------+-------------------+--------------------
+ 1 | 11 | 1 | rls_regress_user1 | my first novel
+ 2 | 11 | 2 | rls_regress_user1 | my second novel
+ 3 | 22 | 2 | rls_regress_user1 | my science fiction
+ 4 | 44 | 1 | rls_regress_user1 | my first manga
+ 5 | 44 | 2 | rls_regress_user1 | my second manga
+ (5 rows)
+
+ SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle) ORDER BY did;
+ NOTICE: f_leak => my first novel
+ NOTICE: f_leak => my second novel
+ NOTICE: f_leak => my science fiction
+ NOTICE: f_leak => my first manga
+ NOTICE: f_leak => my second manga
+ cid | did | dlevel | dauthor | dtitle | cname
+ -----+-----+--------+-------------------+--------------------+-----------------
+ 11 | 1 | 1 | rls_regress_user1 | my first novel | novel
+ 11 | 2 | 2 | rls_regress_user1 | my second novel | novel
+ 22 | 3 | 2 | rls_regress_user1 | my science fiction | science fiction
+ 44 | 4 | 1 | rls_regress_user1 | my first manga | manga
+ 44 | 5 | 2 | rls_regress_user1 | my second manga | manga
+ (5 rows)
+
+ -- viewpoint from rls_regress_user2 again
+ SET SESSION AUTHORIZATION rls_regress_user2;
+ SELECT * FROM document WHERE f_leak(dtitle) ORDER BY did;
+ NOTICE: f_leak => great science fiction
+ NOTICE: f_leak => great technology book
+ NOTICE: f_leak => great manga
+ did | cid | dlevel | dauthor | dtitle
+ -----+-----+--------+-------------------+-----------------------
+ 6 | 22 | 1 | rls_regress_user2 | great science fiction
+ 7 | 33 | 2 | rls_regress_user2 | great technology book
+ 8 | 44 | 1 | rls_regress_user2 | great manga
+ (3 rows)
+
+ SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle) ORDER BY did;
+ NOTICE: f_leak => great science fiction
+ NOTICE: f_leak => great technology book
+ NOTICE: f_leak => great manga
+ cid | did | dlevel | dauthor | dtitle | cname
+ -----+-----+--------+-------------------+-----------------------+-----------------
+ 22 | 6 | 1 | rls_regress_user2 | great science fiction | science fiction
+ 33 | 7 | 2 | rls_regress_user2 | great technology book | technology
+ 44 | 8 | 1 | rls_regress_user2 | great manga | manga
+ (3 rows)
+
+ EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+ QUERY PLAN
+ ----------------------------------------------
+ Subquery Scan on document
+ Filter: f_leak(document.dtitle)
+ -> Seq Scan on document document_1
+ Filter: (dauthor = "current_user"())
+ (4 rows)
+
+ EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+ QUERY PLAN
+ ----------------------------------------------------
+ Nested Loop
+ -> Subquery Scan on document
+ Filter: f_leak(document.dtitle)
+ -> Seq Scan on document document_1
+ Filter: (dauthor = "current_user"())
+ -> Index Scan using category_pkey on category
+ Index Cond: (cid = document.cid)
+ (7 rows)
+
+ -- interaction of FK/PK constraints
+ SET SESSION AUTHORIZATION rls_regress_user0;
+ ALTER TABLE category SET ROW SECURITY FOR ALL
+ TO (CASE WHEN current_user = 'rls_regress_user1' THEN cid IN (11, 33)
+ WHEN current_user = 'rls_regress_user2' THEN cid IN (22, 44)
+ ELSE false END);
+ -- cannot delete PK referenced by invisible FK
+ SET SESSION AUTHORIZATION rls_regress_user1;
+ SELECT * FROM document d full outer join category c on d.cid = c.cid;
+ did | cid | dlevel | dauthor | dtitle | cid | cname
+ -----+-----+--------+-------------------+--------------------+-----+------------
+ 2 | 11 | 2 | rls_regress_user1 | my second novel | 11 | novel
+ 1 | 11 | 1 | rls_regress_user1 | my first novel | 11 | novel
+ | | | | | 33 | technology
+ 5 | 44 | 2 | rls_regress_user1 | my second manga | |
+ 4 | 44 | 1 | rls_regress_user1 | my first manga | |
+ 3 | 22 | 2 | rls_regress_user1 | my science fiction | |
+ (6 rows)
+
+ DELETE FROM category WHERE cid = 33; -- fails with FK violation
+ ERROR: update or delete on table "category" violates foreign key constraint "document_cid_fkey" on table "document"
+ DETAIL: Key (cid)=(33) is still referenced from table "document".
+ -- cannot insert FK referencing invisible PK
+ SET SESSION AUTHORIZATION rls_regress_user2;
+ SELECT * FROM document d full outer join category c on d.cid = c.cid;
+ did | cid | dlevel | dauthor | dtitle | cid | cname
+ -----+-----+--------+-------------------+-----------------------+-----+-----------------
+ 6 | 22 | 1 | rls_regress_user2 | great science fiction | 22 | science fiction
+ 8 | 44 | 1 | rls_regress_user2 | great manga | 44 | manga
+ 7 | 33 | 2 | rls_regress_user2 | great technology book | |
+ (3 rows)
+
+ INSERT INTO document VALUES (10, 33, 1, current_user, 'hoge'); -- fail with FK violation
+ ERROR: insert or update on table "document" violates foreign key constraint "document_cid_fkey"
+ DETAIL: Key (cid)=(33) is not present in table "category".
+ -- UNIQUE or PRIMARY KEY constraint violation DOES reveal presence of row
+ SET SESSION AUTHORIZATION rls_regress_user1;
+ INSERT INTO document VALUES ( 8, 44, 1, 'rls_regress_user_1', 'my third manga' ); -- Must fail with unique violation, revealing presence of did we can't see
+ ERROR: duplicate key value violates unique constraint "document_pkey"
+ DETAIL: Key (did)=(8) already exists.
+ SELECT * FROM document WHERE did = 8; -- and confirm we can't see it
+ did | cid | dlevel | dauthor | dtitle
+ -----+-----+--------+---------+--------
+ (0 rows)
+
+ -- UNIQUE or PRIMARY KEY constraint violation DOES reveal presence of row
+ SET SESSION AUTHORIZATION rls_regress_user1;
+ INSERT INTO document VALUES ( 8, 44, 1, 'rls_regress_user_1', 'my third manga' ); -- Must fail with unique violation, revealing presence of did we can't see
+ ERROR: duplicate key value violates unique constraint "document_pkey"
+ DETAIL: Key (did)=(8) already exists.
+ SELECT * FROM document WHERE did = 8; -- and confirm we can't see it
+ did | cid | dlevel | dauthor | dtitle
+ -----+-----+--------+---------+--------
+ (0 rows)
+
+ -- database superuser can bypass RLS policy
+ RESET SESSION AUTHORIZATION;
+ SELECT * FROM document;
+ did | cid | dlevel | dauthor | dtitle
+ -----+-----+--------+-------------------+-----------------------
+ 1 | 11 | 1 | rls_regress_user1 | my first novel
+ 2 | 11 | 2 | rls_regress_user1 | my second novel
+ 3 | 22 | 2 | rls_regress_user1 | my science fiction
+ 4 | 44 | 1 | rls_regress_user1 | my first manga
+ 5 | 44 | 2 | rls_regress_user1 | my second manga
+ 6 | 22 | 1 | rls_regress_user2 | great science fiction
+ 7 | 33 | 2 | rls_regress_user2 | great technology book
+ 8 | 44 | 1 | rls_regress_user2 | great manga
+ (8 rows)
+
+ SELECT * FROM category;
+ cid | cname
+ -----+-----------------
+ 11 | novel
+ 22 | science fiction
+ 33 | technology
+ 44 | manga
+ (4 rows)
+
+ --
+ -- Table inheritance and RLS policy
+ --
+ SET SESSION AUTHORIZATION rls_regress_user0;
+ CREATE TABLE t1 (a int, junk1 text, b text) WITH OIDS;
+ ALTER TABLE t1 DROP COLUMN junk1; -- just a disturbing factor
+ GRANT ALL ON t1 TO public;
+ COPY t1 FROM stdin WITH (oids);
+ CREATE TABLE t2 (c float) INHERITS (t1);
+ COPY t2 FROM stdin WITH (oids);
+ CREATE TABLE t3 (c text, b text, a int) WITH OIDS;
+ ALTER TABLE t3 INHERIT t1;
+ COPY t3(a,b,c) FROM stdin WITH (oids);
+ ALTER TABLE t1 SET ROW SECURITY FOR ALL TO (a % 2 = 0); -- be even number
+ ALTER TABLE t2 SET ROW SECURITY FOR ALL TO (a % 2 = 1); -- be odd number
+ SELECT * FROM t1;
+ a | b
+ ---+-----
+ 2 | bbb
+ 4 | ddd
+ 2 | bcd
+ 4 | def
+ 2 | yyy
+ (5 rows)
+
+ EXPLAIN (costs off) SELECT * FROM t1;
+ QUERY PLAN
+ -------------------------------
+ Append
+ -> Seq Scan on t1
+ Filter: ((a % 2) = 0)
+ -> Seq Scan on t2
+ Filter: ((a % 2) = 0)
+ -> Seq Scan on t3
+ Filter: ((a % 2) = 0)
+ (7 rows)
+
+ SELECT * FROM t1 WHERE f_leak(b);
+ NOTICE: f_leak => bbb
+ NOTICE: f_leak => ddd
+ NOTICE: f_leak => bcd
+ NOTICE: f_leak => def
+ NOTICE: f_leak => yyy
+ a | b
+ ---+-----
+ 2 | bbb
+ 4 | ddd
+ 2 | bcd
+ 4 | def
+ 2 | yyy
+ (5 rows)
+
+ EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+ QUERY PLAN
+ -------------------------------------
+ Subquery Scan on t1
+ Filter: f_leak(t1.b)
+ -> Append
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Seq Scan on t2
+ Filter: ((a % 2) = 0)
+ -> Seq Scan on t3
+ Filter: ((a % 2) = 0)
+ (9 rows)
+
+ -- reference to system column
+ SELECT oid, * FROM t1;
+ oid | a | b
+ -----+---+-----
+ 102 | 2 | bbb
+ 104 | 4 | ddd
+ 202 | 2 | bcd
+ 204 | 4 | def
+ 302 | 2 | yyy
+ (5 rows)
+
+ EXPLAIN (costs off) SELECT * FROM t1;
+ QUERY PLAN
+ -------------------------------
+ Append
+ -> Seq Scan on t1
+ Filter: ((a % 2) = 0)
+ -> Seq Scan on t2
+ Filter: ((a % 2) = 0)
+ -> Seq Scan on t3
+ Filter: ((a % 2) = 0)
+ (7 rows)
+
+ -- reference to whole-row reference
+ SELECT *,t1 FROM t1;
+ a | b | t1
+ ---+-----+---------
+ 2 | bbb | (2,bbb)
+ 4 | ddd | (4,ddd)
+ 2 | bcd | (2,bcd)
+ 4 | def | (4,def)
+ 2 | yyy | (2,yyy)
+ (5 rows)
+
+ EXPLAIN (costs off) SELECT *,t1 FROM t1;
+ QUERY PLAN
+ -------------------------------
+ Append
+ -> Seq Scan on t1
+ Filter: ((a % 2) = 0)
+ -> Seq Scan on t2
+ Filter: ((a % 2) = 0)
+ -> Seq Scan on t3
+ Filter: ((a % 2) = 0)
+ (7 rows)
+
+ -- for share/update lock
+ SELECT * FROM t1 FOR SHARE;
+ a | b
+ ---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+ (7 rows)
+
+ EXPLAIN (costs off) SELECT * FROM t1 FOR SHARE;
+ QUERY PLAN
+ -------------------------------------------
+ LockRows
+ -> Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+ (9 rows)
+
+ SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+ NOTICE: f_leak => bbb
+ NOTICE: f_leak => ddd
+ NOTICE: f_leak => abc
+ NOTICE: f_leak => cde
+ NOTICE: f_leak => xxx
+ NOTICE: f_leak => yyy
+ NOTICE: f_leak => zzz
+ a | b
+ ---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+ (7 rows)
+
+ EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+ QUERY PLAN
+ -------------------------------------------
+ LockRows
+ -> Append
+ -> Subquery Scan on t1
+ Filter: f_leak(t1.b)
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ Filter: f_leak(t2.b)
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+ Filter: f_leak(b)
+ (12 rows)
+
+ ----- Dependencies -----
+ CREATE TABLE dependee (x integer, y integer);
+ CREATE TABLE dependent(x integer, y integer);
+ ALTER TABLE dependent SET ROW SECURITY FOR ALL TO (x = (SELECT d.x FROM dependee d WHERE d.y = y));
+ DROP TABLE dependee; -- Should fail without CASCADE due to dependency on row-security qual?
+ ERROR: cannot drop table dependee because other objects depend on it
+ DETAIL: row-security of table dependent FOR ALL depends on table dependee
+ HINT: Use DROP ... CASCADE to drop the dependent objects too.
+ EXPLAIN SELECT * FROM dependent; -- After drop, should be unqualified
+ QUERY PLAN
+ -------------------------------------------------------------
+ Seq Scan on dependent (cost=0.00..31.40 rows=2140 width=8)
+ Planning time: 0.078 ms
+ (2 rows)
+
+ ----- RECURSION ----
+ --
+ -- Simple recursion
+ --
+ CREATE TABLE rec1 (x integer, y integer);
+ ALTER TABLE rec1
+ SET ROW SECURITY FOR ALL TO (
+ x = (SELECT r.x FROM rec1 r WHERE y = r.y)
+ );
+ SELECT * FROM rec1; -- fail, direct recursion
+ ERROR: infinite recursion detected in row-security policy for relation "rec1"
+ --
+ -- Mutual recursion
+ --
+ CREATE TABLE rec2 (a integer, b integer);
+ ALTER TABLE rec1 SET ROW SECURITY FOR ALL TO (x = (SELECT a FROM rec2 WHERE b = y));
+ ALTER TABLE rec2 SET ROW SECURITY FOR ALL TO (a = (SELECT x FROM rec1 WHERE y = b));
+ SELECT * FROM rec1; -- fail, mutual recursion
+ ERROR: infinite recursion detected in row-security policy for relation "rec1"
+ --
+ -- Mutual recursion via views
+ --
+ CREATE VIEW rec1v AS SELECT * FROM rec1;
+ CREATE VIEW rec2v AS SELECT * FROM rec2;
+ ALTER TABLE rec1 SET ROW SECURITY FOR ALL TO (x = (SELECT a FROM rec2v WHERE b = y));
+ ALTER TABLE rec2 SET ROW SECURITY FOR ALL TO (a = (SELECT x FROM rec1v WHERE y = b));
+ SELECT * FROM rec1; -- fail, mutual recursion via views
+ ERROR: infinite recursion detected in row-security policy for relation "rec1"
+ --
+ -- Mutual recursion via .s.b views
+ --
+ DROP VIEW rec1v, rec2v CASCADE;
+ NOTICE: drop cascades to 2 other objects
+ DETAIL: drop cascades to row-security of table rec1 FOR ALL
+ drop cascades to row-security of table rec2 FOR ALL
+ CREATE VIEW rec1v WITH (security_barrier) AS SELECT * FROM rec1;
+ CREATE VIEW rec2v WITH (security_barrier) AS SELECT * FROM rec2;
+ ALTER TABLE rec1 SET ROW SECURITY FOR ALL TO (x = (SELECT a FROM rec2v WHERE b = y));
+ ALTER TABLE rec2 SET ROW SECURITY FOR ALL TO (a = (SELECT x FROM rec1v WHERE y = b));
+ SELECT * FROM rec1; -- fail, mutual recursion via s.b. views
+ ERROR: infinite recursion detected in row-security policy for relation "rec1"
+ --
+ -- recursive RLS and VIEWs in policy
+ --
+ CREATE TABLE s1 (a int, b text);
+ INSERT INTO s1 (SELECT x, md5(x::text) FROM generate_series(-10,10) x);
+ CREATE TABLE s2 (x int, y text);
+ INSERT INTO s2 (SELECT x, md5(x::text) FROM generate_series(-6,6) x);
+ CREATE VIEW v2 AS SELECT * FROM s2 WHERE y like '%af%';
+ ALTER TABLE s1 SET ROW SECURITY FOR ALL
+ TO (a in (select x from s2 where y like '%2f%'));
+ ALTER TABLE s2 SET ROW SECURITY FOR ALL
+ TO (x in (select a from s1 where b like '%22%'));
+ SELECT * FROM s1 WHERE f_leak(b); -- fail (infinite recursion)
+ ERROR: infinite recursion detected for relation "s1"
+ ALTER TABLE s2 SET ROW SECURITY FOR ALL TO (x % 2 = 0);
+ SELECT * FROM s1 WHERE f_leak(b); -- OK
+ NOTICE: f_leak => c81e728d9d4c2f636f067f89cc14862c
+ NOTICE: f_leak => a87ff679a2f3e71d9181a67b7542122c
+ a | b
+ ---+----------------------------------
+ 2 | c81e728d9d4c2f636f067f89cc14862c
+ 4 | a87ff679a2f3e71d9181a67b7542122c
+ (2 rows)
+
+ EXPLAIN SELECT * FROM only s1 WHERE f_leak(b);
+ QUERY PLAN
+ ---------------------------------------------------------------------------------------
+ Subquery Scan on s1 (cost=28.55..61.67 rows=205 width=36)
+ Filter: f_leak(s1.b)
+ -> Hash Join (cost=28.55..55.52 rows=615 width=36)
+ Hash Cond: (s1_1.a = s2.x)
+ -> Seq Scan on s1 s1_1 (cost=0.00..22.30 rows=1230 width=36)
+ -> Hash (cost=28.54..28.54 rows=1 width=4)
+ -> HashAggregate (cost=28.53..28.54 rows=1 width=4)
+ Group Key: s2.x
+ -> Subquery Scan on s2 (cost=0.00..28.52 rows=1 width=4)
+ Filter: (s2.y ~~ '%2f%'::text)
+ -> Seq Scan on s2 s2_1 (cost=0.00..28.45 rows=6 width=36)
+ Filter: ((x % 2) = 0)
+ (12 rows)
+
+ ALTER TABLE s1 SET ROW SECURITY FOR ALL
+ TO (a in (select x from v2)); -- using VIEW in RLS policy
+ SELECT * FROM s1 WHERE f_leak(b); -- OK
+ NOTICE: f_leak => 0267aaf632e87a63288a08331f22c7c3
+ NOTICE: f_leak => 1679091c5a880faf6fb5e6087eb1b2dc
+ a | b
+ ----+----------------------------------
+ -4 | 0267aaf632e87a63288a08331f22c7c3
+ 6 | 1679091c5a880faf6fb5e6087eb1b2dc
+ (2 rows)
+
+ EXPLAIN (COSTS OFF) SELECT * FROM s1 WHERE f_leak(b);
+ QUERY PLAN
+ ----------------------------------------------------------
+ Subquery Scan on s1
+ Filter: f_leak(s1.b)
+ -> Hash Join
+ Hash Cond: (s1_1.a = s2.x)
+ -> Seq Scan on s1 s1_1
+ -> Hash
+ -> HashAggregate
+ Group Key: s2.x
+ -> Subquery Scan on s2
+ Filter: (s2.y ~~ '%af%'::text)
+ -> Seq Scan on s2 s2_1
+ Filter: ((x % 2) = 0)
+ (12 rows)
+
+ SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+ xx | x | y
+ ----+----+----------------------------------
+ -6 | -6 | 596a3d04481816330f07e4f97510c28f
+ -4 | -4 | 0267aaf632e87a63288a08331f22c7c3
+ 2 | 2 | c81e728d9d4c2f636f067f89cc14862c
+ (3 rows)
+
+ EXPLAIN (COSTS OFF) SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+ QUERY PLAN
+ --------------------------------------------------------------------
+ Subquery Scan on s2
+ Filter: (s2.y ~~ '%28%'::text)
+ -> Seq Scan on s2 s2_1
+ Filter: ((x % 2) = 0)
+ SubPlan 1
+ -> Limit
+ -> Subquery Scan on s1
+ -> Nested Loop Semi Join
+ Join Filter: (s1_1.a = s2_2.x)
+ -> Seq Scan on s1 s1_1
+ -> Materialize
+ -> Subquery Scan on s2_2
+ Filter: (s2_2.y ~~ '%af%'::text)
+ -> Seq Scan on s2 s2_3
+ Filter: ((x % 2) = 0)
+ (15 rows)
+
+ ALTER TABLE s2 SET ROW SECURITY FOR ALL
+ TO (x in (select a from s1 where b like '%d2%'));
+ SELECT * FROM s1 WHERE f_leak(b); -- fail (infinite recursion via view)
+ ERROR: infinite recursion detected for relation "s1"
+ -- prepared statement with rls_regress_user0 privilege
+ PREPARE p1(int) AS SELECT * FROM t1 WHERE a <= $1;
+ EXECUTE p1(2);
+ a | b
+ ---+-----
+ 2 | bbb
+ 1 | abc
+ 1 | xxx
+ 2 | yyy
+ (4 rows)
+
+ EXPLAIN (costs off) EXECUTE p1(2);
+ QUERY PLAN
+ ----------------------------------------------------
+ Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a <= 2) AND ((a % 2) = 0))
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a <= 2) AND ((a % 2) = 1))
+ -> Seq Scan on t3
+ Filter: (a <= 2)
+ (9 rows)
+
+ -- superuser is allowed to bypass RLS checks
+ RESET SESSION AUTHORIZATION;
+ SELECT * FROM t1 WHERE f_leak(b);
+ NOTICE: f_leak => aaa
+ NOTICE: f_leak => bbb
+ NOTICE: f_leak => ccc
+ NOTICE: f_leak => ddd
+ NOTICE: f_leak => abc
+ NOTICE: f_leak => bcd
+ NOTICE: f_leak => cde
+ NOTICE: f_leak => def
+ NOTICE: f_leak => xxx
+ NOTICE: f_leak => yyy
+ NOTICE: f_leak => zzz
+ a | b
+ ---+-----
+ 1 | aaa
+ 2 | bbb
+ 3 | ccc
+ 4 | ddd
+ 1 | abc
+ 2 | bcd
+ 3 | cde
+ 4 | def
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+ (11 rows)
+
+ EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+ QUERY PLAN
+ ---------------------------
+ Append
+ -> Seq Scan on t1
+ Filter: f_leak(b)
+ -> Seq Scan on t2
+ Filter: f_leak(b)
+ -> Seq Scan on t3
+ Filter: f_leak(b)
+ (7 rows)
+
+ -- plan cache should be invalidated
+ EXECUTE p1(2);
+ a | b
+ ---+-----
+ 1 | aaa
+ 2 | bbb
+ 1 | abc
+ 2 | bcd
+ 1 | xxx
+ 2 | yyy
+ (6 rows)
+
+ EXPLAIN (costs off) EXECUTE p1(2);
+ QUERY PLAN
+ --------------------------
+ Append
+ -> Seq Scan on t1
+ Filter: (a <= 2)
+ -> Seq Scan on t2
+ Filter: (a <= 2)
+ -> Seq Scan on t3
+ Filter: (a <= 2)
+ (7 rows)
+
+ PREPARE p2(int) AS SELECT * FROM t1 WHERE a = $1;
+ EXECUTE p2(2);
+ a | b
+ ---+-----
+ 2 | bbb
+ 2 | bcd
+ 2 | yyy
+ (3 rows)
+
+ EXPLAIN (costs off) EXECUTE p2(2);
+ QUERY PLAN
+ -------------------------
+ Append
+ -> Seq Scan on t1
+ Filter: (a = 2)
+ -> Seq Scan on t2
+ Filter: (a = 2)
+ -> Seq Scan on t3
+ Filter: (a = 2)
+ (7 rows)
+
+ -- also, case when privilege switch from superuser
+ SET SESSION AUTHORIZATION rls_regress_user0;
+ EXECUTE p2(2);
+ a | b
+ ---+-----
+ 2 | bbb
+ 2 | yyy
+ (2 rows)
+
+ EXPLAIN (costs off) EXECUTE p2(2);
+ QUERY PLAN
+ ---------------------------------------------------
+ Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a = 2) AND ((a % 2) = 0))
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a = 2) AND ((a % 2) = 1))
+ -> Seq Scan on t3
+ Filter: (a = 2)
+ (9 rows)
+
+ --
+ -- UPDATE / DELETE and Row-level security
+ --
+ SET SESSION AUTHORIZATION rls_regress_user0;
+ EXPLAIN (costs off) UPDATE t1 SET b = b || b WHERE f_leak(b);
+ QUERY PLAN
+ -------------------------------------
+ Update on t1
+ -> Subquery Scan on t1_1
+ Filter: f_leak(t1_1.b)
+ -> Seq Scan on t1 t1_2
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ Filter: f_leak(t2.b)
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+ Filter: f_leak(b)
+ (11 rows)
+
+ UPDATE t1 SET b = b || b WHERE f_leak(b);
+ NOTICE: f_leak => bbb
+ NOTICE: f_leak => ddd
+ NOTICE: f_leak => abc
+ NOTICE: f_leak => cde
+ NOTICE: f_leak => xxx
+ NOTICE: f_leak => yyy
+ NOTICE: f_leak => zzz
+ EXPLAIN (costs off) UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b);
+ QUERY PLAN
+ -------------------------------------
+ Update on t1
+ -> Subquery Scan on t1_1
+ Filter: f_leak(t1_1.b)
+ -> Seq Scan on t1 t1_2
+ Filter: ((a % 2) = 0)
+ (5 rows)
+
+ UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b);
+ NOTICE: f_leak => bbbbbb
+ NOTICE: f_leak => dddddd
+ -- returning clause with system column
+ UPDATE only t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+ NOTICE: f_leak => bbbbbb_updt
+ NOTICE: f_leak => dddddd_updt
+ oid | a | b | t1
+ -----+---+-------------+-----------------
+ 102 | 2 | bbbbbb_updt | (2,bbbbbb_updt)
+ 104 | 4 | dddddd_updt | (4,dddddd_updt)
+ (2 rows)
+
+ UPDATE t1 SET b = b WHERE f_leak(b) RETURNING *;
+ NOTICE: f_leak => bbbbbb_updt
+ NOTICE: f_leak => dddddd_updt
+ NOTICE: f_leak => abcabc
+ NOTICE: f_leak => cdecde
+ NOTICE: f_leak => xxxxxx
+ NOTICE: f_leak => yyyyyy
+ NOTICE: f_leak => zzzzzz
+ a | b
+ ---+-------------
+ 2 | bbbbbb_updt
+ 4 | dddddd_updt
+ 1 | abcabc
+ 3 | cdecde
+ 1 | xxxxxx
+ 2 | yyyyyy
+ 3 | zzzzzz
+ (7 rows)
+
+ UPDATE t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+ NOTICE: f_leak => bbbbbb_updt
+ NOTICE: f_leak => dddddd_updt
+ NOTICE: f_leak => abcabc
+ NOTICE: f_leak => cdecde
+ NOTICE: f_leak => xxxxxx
+ NOTICE: f_leak => yyyyyy
+ NOTICE: f_leak => zzzzzz
+ oid | a | b | t1
+ -----+---+-------------+-----------------
+ 102 | 2 | bbbbbb_updt | (2,bbbbbb_updt)
+ 104 | 4 | dddddd_updt | (4,dddddd_updt)
+ 201 | 1 | abcabc | (1,abcabc)
+ 203 | 3 | cdecde | (3,cdecde)
+ 301 | 1 | xxxxxx | (1,xxxxxx)
+ 302 | 2 | yyyyyy | (2,yyyyyy)
+ 303 | 3 | zzzzzz | (3,zzzzzz)
+ (7 rows)
+
+ RESET SESSION AUTHORIZATION;
+ SELECT * FROM t1;
+ a | b
+ ---+-------------
+ 1 | aaa
+ 3 | ccc
+ 2 | bbbbbb_updt
+ 4 | dddddd_updt
+ 2 | bcd
+ 4 | def
+ 1 | abcabc
+ 3 | cdecde
+ 1 | xxxxxx
+ 2 | yyyyyy
+ 3 | zzzzzz
+ (11 rows)
+
+ SET SESSION AUTHORIZATION rls_regress_user0;
+ EXPLAIN (costs off) DELETE FROM only t1 WHERE f_leak(b);
+ QUERY PLAN
+ -------------------------------------
+ Delete on t1
+ -> Subquery Scan on t1_1
+ Filter: f_leak(t1_1.b)
+ -> Seq Scan on t1 t1_2
+ Filter: ((a % 2) = 0)
+ (5 rows)
+
+ EXPLAIN (costs off) DELETE FROM t1 WHERE f_leak(b);
+ QUERY PLAN
+ -------------------------------------
+ Delete on t1
+ -> Subquery Scan on t1_1
+ Filter: f_leak(t1_1.b)
+ -> Seq Scan on t1 t1_2
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ Filter: f_leak(t2.b)
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+ Filter: f_leak(b)
+ (11 rows)
+
+ DELETE FROM only t1 WHERE f_leak(b) RETURNING oid, *, t1;
+ NOTICE: f_leak => bbbbbb_updt
+ NOTICE: f_leak => dddddd_updt
+ oid | a | b | t1
+ -----+---+-------------+-----------------
+ 102 | 2 | bbbbbb_updt | (2,bbbbbb_updt)
+ 104 | 4 | dddddd_updt | (4,dddddd_updt)
+ (2 rows)
+
+ DELETE FROM t1 WHERE f_leak(b) RETURNING oid, *, t1;
+ NOTICE: f_leak => abcabc
+ NOTICE: f_leak => cdecde
+ NOTICE: f_leak => xxxxxx
+ NOTICE: f_leak => yyyyyy
+ NOTICE: f_leak => zzzzzz
+ oid | a | b | t1
+ -----+---+--------+------------
+ 201 | 1 | abcabc | (1,abcabc)
+ 203 | 3 | cdecde | (3,cdecde)
+ 301 | 1 | xxxxxx | (1,xxxxxx)
+ 302 | 2 | yyyyyy | (2,yyyyyy)
+ 303 | 3 | zzzzzz | (3,zzzzzz)
+ (5 rows)
+
+ --
+ -- Test psql \dt+ command
+ --
+ ALTER TABLE category RESET ROW SECURITY FOR ALL; -- too long qual
+ \dt+
+ List of relations
+ Schema | Name | Type | Owner | Size | Description | Row-security
+ --------------------+----------+-------+-------------------+------------+-------------+----------------------------------
+ rls_regress_schema | category | table | rls_regress_user0 | 16 kB | |
+ rls_regress_schema | document | table | rls_regress_user0 | 16 kB | | (dauthor = "current_user"())
+ rls_regress_schema | s1 | table | rls_regress_user0 | 16 kB | | (a IN ( SELECT v2.x +
+ | | | | | | FROM v2))
+ rls_regress_schema | s2 | table | rls_regress_user0 | 16 kB | | (x IN ( SELECT s1.a +
+ | | | | | | FROM s1 +
+ | | | | | | WHERE (s1.b ~~ '%d2%'::text)))
+ rls_regress_schema | t1 | table | rls_regress_user0 | 16 kB | | ((a % 2) = 0)
+ rls_regress_schema | t2 | table | rls_regress_user0 | 16 kB | | ((a % 2) = 1)
+ rls_regress_schema | t3 | table | rls_regress_user0 | 16 kB | |
+ rls_regress_schema | uaccount | table | rls_regress_user0 | 8192 bytes | |
+ (8 rows)
+
+ --
+ -- Clean up objects
+ --
+ RESET SESSION AUTHORIZATION;
+ DROP SCHEMA rls_regress_schema CASCADE;
+ NOTICE: drop cascades to 10 other objects
+ DETAIL: drop cascades to function f_leak(text)
+ drop cascades to table uaccount
+ drop cascades to table category
+ drop cascades to table document
+ drop cascades to table t1
+ drop cascades to table t2
+ drop cascades to table t3
+ drop cascades to table s1
+ drop cascades to table s2
+ drop cascades to view v2
+ DROP USER rls_regress_user0;
+ DROP USER rls_regress_user1;
+ DROP USER rls_regress_user2;
diff --git a/src/test/regress/expected/sanity_check.out b/src/test/regress/expected/sanity_check.out
new file mode 100644
index 111d24c..2c8ec11 100644
*** a/src/test/regress/expected/sanity_check.out
--- b/src/test/regress/expected/sanity_check.out
*************** pg_pltemplate|t
*** 121,126 ****
--- 121,127 ----
pg_proc|t
pg_range|t
pg_rewrite|t
+ pg_rowsecurity|t
pg_seclabel|t
pg_shdepend|t
pg_shdescription|t
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
new file mode 100644
index c0416f4..8d4cd05 100644
*** a/src/test/regress/parallel_schedule
--- b/src/test/regress/parallel_schedule
*************** test: select_into select_distinct select
*** 83,89 ****
# ----------
# Another group of parallel tests
# ----------
! test: privileges security_label collate matview lock replica_identity
# ----------
# Another group of parallel tests
--- 83,89 ----
# ----------
# Another group of parallel tests
# ----------
! test: privileges rowsecurity security_label collate matview lock replica_identity
# ----------
# Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
new file mode 100644
index 16a1905..9c45b8b 100644
*** a/src/test/regress/serial_schedule
--- b/src/test/regress/serial_schedule
*************** test: delete
*** 96,101 ****
--- 96,102 ----
test: namespace
test: prepared_xacts
test: privileges
+ test: rowsecurity
test: security_label
test: collate
test: matview
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
new file mode 100644
index ...61e4ca8 .
*** a/src/test/regress/sql/rowsecurity.sql
--- b/src/test/regress/sql/rowsecurity.sql
***************
*** 0 ****
--- 1,355 ----
+ --
+ -- Test of Row-level security feature
+ --
+
+ -- Clean up in case a prior regression run failed
+
+ -- Suppress NOTICE messages when users/groups don't exist
+ SET client_min_messages TO 'warning';
+
+ DROP USER IF EXISTS rls_regress_user0;
+ DROP USER IF EXISTS rls_regress_user1;
+ DROP USER IF EXISTS rls_regress_user2;
+
+ DROP SCHEMA IF EXISTS rls_regress_schema CASCADE;
+
+ RESET client_min_messages;
+
+ -- initial setup
+ CREATE USER rls_regress_user0;
+ CREATE USER rls_regress_user1;
+ CREATE USER rls_regress_user2;
+
+ CREATE SCHEMA rls_regress_schema;
+ GRANT ALL ON SCHEMA rls_regress_schema TO public;
+ SET search_path = rls_regress_schema;
+
+ -- setup of malicious function
+ CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool
+ COST 0.0000001 LANGUAGE plpgsql
+ AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END';
+ GRANT EXECUTE ON FUNCTION f_leak(text) TO public;
+
+ -- BASIC Row-Level Security Scenario
+
+ SET SESSION AUTHORIZATION rls_regress_user0;
+ CREATE TABLE uaccount (
+ pguser name primary key,
+ seclv int
+ );
+ INSERT INTO uaccount VALUES
+ ('rls_regress_user0', 99),
+ ('rls_regress_user1', 1),
+ ('rls_regress_user2', 2),
+ ('rls_regress_user3', 3);
+ GRANT SELECT ON uaccount TO public;
+
+ CREATE TABLE category (
+ cid int primary key,
+ cname text
+ );
+ GRANT ALL ON category TO public;
+ INSERT INTO category VALUES
+ (11, 'novel'),
+ (22, 'science fiction'),
+ (33, 'technology'),
+ (44, 'manga');
+
+ CREATE TABLE document (
+ did int primary key,
+ cid int references category(cid),
+ dlevel int not null,
+ dauthor name,
+ dtitle text
+ );
+ GRANT ALL ON document TO public;
+ INSERT INTO document VALUES
+ ( 1, 11, 1, 'rls_regress_user1', 'my first novel'),
+ ( 2, 11, 2, 'rls_regress_user1', 'my second novel'),
+ ( 3, 22, 2, 'rls_regress_user1', 'my science fiction'),
+ ( 4, 44, 1, 'rls_regress_user1', 'my first manga'),
+ ( 5, 44, 2, 'rls_regress_user1', 'my second manga'),
+ ( 6, 22, 1, 'rls_regress_user2', 'great science fiction'),
+ ( 7, 33, 2, 'rls_regress_user2', 'great technology book'),
+ ( 8, 44, 1, 'rls_regress_user2', 'great manga');
+
+ -- user's security level must higher than or equal to document's one
+ ALTER TABLE document SET ROW SECURITY FOR ALL
+ TO (dlevel <= (SELECT seclv FROM uaccount WHERE pguser = current_user));
+
+ -- viewpoint from rls_regress_user1
+ SET SESSION AUTHORIZATION rls_regress_user1;
+ SELECT * FROM document WHERE f_leak(dtitle) ORDER BY did;
+ SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle) ORDER BY did;
+
+ -- viewpoint from rls_regress_user2
+ SET SESSION AUTHORIZATION rls_regress_user2;
+ SELECT * FROM document WHERE f_leak(dtitle) ORDER BY did;
+ SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle) ORDER BY did;
+
+ EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+ EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+ -- only owner can change row-level security
+ ALTER TABLE document SET ROW SECURITY FOR ALL TO (true); -- fail
+ ALTER TABLE document RESET ROW SECURITY FOR ALL; -- fail
+
+ SET SESSION AUTHORIZATION rls_regress_user0;
+ ALTER TABLE document SET ROW SECURITY FOR ALL TO (dauthor = current_user);
+
+ -- viewpoint from rls_regress_user1 again
+ SET SESSION AUTHORIZATION rls_regress_user1;
+ SELECT * FROM document WHERE f_leak(dtitle) ORDER BY did;
+ SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle) ORDER BY did;
+
+ -- viewpoint from rls_regress_user2 again
+ SET SESSION AUTHORIZATION rls_regress_user2;
+ SELECT * FROM document WHERE f_leak(dtitle) ORDER BY did;
+ SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle) ORDER BY did;
+
+ EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+ EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+ -- interaction of FK/PK constraints
+ SET SESSION AUTHORIZATION rls_regress_user0;
+ ALTER TABLE category SET ROW SECURITY FOR ALL
+ TO (CASE WHEN current_user = 'rls_regress_user1' THEN cid IN (11, 33)
+ WHEN current_user = 'rls_regress_user2' THEN cid IN (22, 44)
+ ELSE false END);
+
+ -- cannot delete PK referenced by invisible FK
+ SET SESSION AUTHORIZATION rls_regress_user1;
+ SELECT * FROM document d full outer join category c on d.cid = c.cid;
+ DELETE FROM category WHERE cid = 33; -- fails with FK violation
+
+ -- cannot insert FK referencing invisible PK
+ SET SESSION AUTHORIZATION rls_regress_user2;
+ SELECT * FROM document d full outer join category c on d.cid = c.cid;
+ INSERT INTO document VALUES (10, 33, 1, current_user, 'hoge'); -- fail with FK violation
+
+ -- UNIQUE or PRIMARY KEY constraint violation DOES reveal presence of row
+ SET SESSION AUTHORIZATION rls_regress_user1;
+ INSERT INTO document VALUES ( 8, 44, 1, 'rls_regress_user_1', 'my third manga' ); -- Must fail with unique violation, revealing presence of did we can't see
+ SELECT * FROM document WHERE did = 8; -- and confirm we can't see it
+
+ -- UNIQUE or PRIMARY KEY constraint violation DOES reveal presence of row
+ SET SESSION AUTHORIZATION rls_regress_user1;
+ INSERT INTO document VALUES ( 8, 44, 1, 'rls_regress_user_1', 'my third manga' ); -- Must fail with unique violation, revealing presence of did we can't see
+ SELECT * FROM document WHERE did = 8; -- and confirm we can't see it
+
+ -- database superuser can bypass RLS policy
+ RESET SESSION AUTHORIZATION;
+ SELECT * FROM document;
+ SELECT * FROM category;
+
+ --
+ -- Table inheritance and RLS policy
+ --
+ SET SESSION AUTHORIZATION rls_regress_user0;
+
+ CREATE TABLE t1 (a int, junk1 text, b text) WITH OIDS;
+ ALTER TABLE t1 DROP COLUMN junk1; -- just a disturbing factor
+ GRANT ALL ON t1 TO public;
+
+ COPY t1 FROM stdin WITH (oids);
+ 101 1 aaa
+ 102 2 bbb
+ 103 3 ccc
+ 104 4 ddd
+ \.
+
+ CREATE TABLE t2 (c float) INHERITS (t1);
+ COPY t2 FROM stdin WITH (oids);
+ 201 1 abc 1.1
+ 202 2 bcd 2.2
+ 203 3 cde 3.3
+ 204 4 def 4.4
+ \.
+
+ CREATE TABLE t3 (c text, b text, a int) WITH OIDS;
+ ALTER TABLE t3 INHERIT t1;
+ COPY t3(a,b,c) FROM stdin WITH (oids);
+ 301 1 xxx X
+ 302 2 yyy Y
+ 303 3 zzz Z
+ \.
+
+ ALTER TABLE t1 SET ROW SECURITY FOR ALL TO (a % 2 = 0); -- be even number
+ ALTER TABLE t2 SET ROW SECURITY FOR ALL TO (a % 2 = 1); -- be odd number
+
+ SELECT * FROM t1;
+ EXPLAIN (costs off) SELECT * FROM t1;
+
+ SELECT * FROM t1 WHERE f_leak(b);
+ EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+
+ -- reference to system column
+ SELECT oid, * FROM t1;
+ EXPLAIN (costs off) SELECT * FROM t1;
+
+ -- reference to whole-row reference
+ SELECT *,t1 FROM t1;
+ EXPLAIN (costs off) SELECT *,t1 FROM t1;
+
+ -- for share/update lock
+ SELECT * FROM t1 FOR SHARE;
+ EXPLAIN (costs off) SELECT * FROM t1 FOR SHARE;
+
+ SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+ EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+
+ ----- Dependencies -----
+
+ CREATE TABLE dependee (x integer, y integer);
+
+ CREATE TABLE dependent(x integer, y integer);
+ ALTER TABLE dependent SET ROW SECURITY FOR ALL TO (x = (SELECT d.x FROM dependee d WHERE d.y = y));
+
+ DROP TABLE dependee; -- Should fail without CASCADE due to dependency on row-security qual?
+
+ EXPLAIN SELECT * FROM dependent; -- After drop, should be unqualified
+
+ ----- RECURSION ----
+
+ --
+ -- Simple recursion
+ --
+
+ CREATE TABLE rec1 (x integer, y integer);
+ ALTER TABLE rec1
+ SET ROW SECURITY FOR ALL TO (
+ x = (SELECT r.x FROM rec1 r WHERE y = r.y)
+ );
+
+ SELECT * FROM rec1; -- fail, direct recursion
+
+ --
+ -- Mutual recursion
+ --
+ CREATE TABLE rec2 (a integer, b integer);
+ ALTER TABLE rec1 SET ROW SECURITY FOR ALL TO (x = (SELECT a FROM rec2 WHERE b = y));
+ ALTER TABLE rec2 SET ROW SECURITY FOR ALL TO (a = (SELECT x FROM rec1 WHERE y = b));
+
+ SELECT * FROM rec1; -- fail, mutual recursion
+
+ --
+ -- Mutual recursion via views
+ --
+ CREATE VIEW rec1v AS SELECT * FROM rec1;
+ CREATE VIEW rec2v AS SELECT * FROM rec2;
+ ALTER TABLE rec1 SET ROW SECURITY FOR ALL TO (x = (SELECT a FROM rec2v WHERE b = y));
+ ALTER TABLE rec2 SET ROW SECURITY FOR ALL TO (a = (SELECT x FROM rec1v WHERE y = b));
+
+ SELECT * FROM rec1; -- fail, mutual recursion via views
+
+ --
+ -- Mutual recursion via .s.b views
+ --
+
+ DROP VIEW rec1v, rec2v CASCADE;
+ CREATE VIEW rec1v WITH (security_barrier) AS SELECT * FROM rec1;
+ CREATE VIEW rec2v WITH (security_barrier) AS SELECT * FROM rec2;
+ ALTER TABLE rec1 SET ROW SECURITY FOR ALL TO (x = (SELECT a FROM rec2v WHERE b = y));
+ ALTER TABLE rec2 SET ROW SECURITY FOR ALL TO (a = (SELECT x FROM rec1v WHERE y = b));
+
+ SELECT * FROM rec1; -- fail, mutual recursion via s.b. views
+
+ --
+ -- recursive RLS and VIEWs in policy
+ --
+ CREATE TABLE s1 (a int, b text);
+ INSERT INTO s1 (SELECT x, md5(x::text) FROM generate_series(-10,10) x);
+
+ CREATE TABLE s2 (x int, y text);
+ INSERT INTO s2 (SELECT x, md5(x::text) FROM generate_series(-6,6) x);
+ CREATE VIEW v2 AS SELECT * FROM s2 WHERE y like '%af%';
+
+ ALTER TABLE s1 SET ROW SECURITY FOR ALL
+ TO (a in (select x from s2 where y like '%2f%'));
+
+ ALTER TABLE s2 SET ROW SECURITY FOR ALL
+ TO (x in (select a from s1 where b like '%22%'));
+
+ SELECT * FROM s1 WHERE f_leak(b); -- fail (infinite recursion)
+
+ ALTER TABLE s2 SET ROW SECURITY FOR ALL TO (x % 2 = 0);
+
+ SELECT * FROM s1 WHERE f_leak(b); -- OK
+ EXPLAIN SELECT * FROM only s1 WHERE f_leak(b);
+
+ ALTER TABLE s1 SET ROW SECURITY FOR ALL
+ TO (a in (select x from v2)); -- using VIEW in RLS policy
+ SELECT * FROM s1 WHERE f_leak(b); -- OK
+ EXPLAIN (COSTS OFF) SELECT * FROM s1 WHERE f_leak(b);
+
+ SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+ EXPLAIN (COSTS OFF) SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+
+ ALTER TABLE s2 SET ROW SECURITY FOR ALL
+ TO (x in (select a from s1 where b like '%d2%'));
+ SELECT * FROM s1 WHERE f_leak(b); -- fail (infinite recursion via view)
+
+ -- prepared statement with rls_regress_user0 privilege
+ PREPARE p1(int) AS SELECT * FROM t1 WHERE a <= $1;
+ EXECUTE p1(2);
+ EXPLAIN (costs off) EXECUTE p1(2);
+
+ -- superuser is allowed to bypass RLS checks
+ RESET SESSION AUTHORIZATION;
+ SELECT * FROM t1 WHERE f_leak(b);
+ EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+
+ -- plan cache should be invalidated
+ EXECUTE p1(2);
+ EXPLAIN (costs off) EXECUTE p1(2);
+
+ PREPARE p2(int) AS SELECT * FROM t1 WHERE a = $1;
+ EXECUTE p2(2);
+ EXPLAIN (costs off) EXECUTE p2(2);
+
+ -- also, case when privilege switch from superuser
+ SET SESSION AUTHORIZATION rls_regress_user0;
+ EXECUTE p2(2);
+ EXPLAIN (costs off) EXECUTE p2(2);
+
+ --
+ -- UPDATE / DELETE and Row-level security
+ --
+ SET SESSION AUTHORIZATION rls_regress_user0;
+ EXPLAIN (costs off) UPDATE t1 SET b = b || b WHERE f_leak(b);
+ UPDATE t1 SET b = b || b WHERE f_leak(b);
+
+ EXPLAIN (costs off) UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b);
+ UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b);
+
+ -- returning clause with system column
+ UPDATE only t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+ UPDATE t1 SET b = b WHERE f_leak(b) RETURNING *;
+ UPDATE t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+
+ RESET SESSION AUTHORIZATION;
+ SELECT * FROM t1;
+
+ SET SESSION AUTHORIZATION rls_regress_user0;
+ EXPLAIN (costs off) DELETE FROM only t1 WHERE f_leak(b);
+ EXPLAIN (costs off) DELETE FROM t1 WHERE f_leak(b);
+
+ DELETE FROM only t1 WHERE f_leak(b) RETURNING oid, *, t1;
+ DELETE FROM t1 WHERE f_leak(b) RETURNING oid, *, t1;
+
+ --
+ -- Test psql \dt+ command
+ --
+ ALTER TABLE category RESET ROW SECURITY FOR ALL; -- too long qual
+ \dt+
+
+ --
+ -- 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;