diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml new file mode 100644 index acc261c..631c5d2 *** a/doc/src/sgml/catalogs.sgml --- b/doc/src/sgml/catalogs.sgml *************** *** 234,239 **** --- 234,244 ---- + pg_rowsecuritylevelsec + row-level security policy of relation + + + pg_seclabel security labels on database objects *************** *** 1855,1860 **** --- 1860,1875 ---- + relhasrowsecurity + bool + + + True if table has row-security policy; see + pg_rowsecurity catalog + + + + relhassubclass bool *************** *** 5135,5140 **** --- 5150,5205 ---- + + <structname>pg_rowsecurity</structname> + + + pg_rowsecurity + + + The catalog pg_rowsecurity stores expression + tree of row-security policy to be performed on a particular relation. + + + <structname>pg_rowsecurity</structname> Columns + + + + Name + Type + References + Description + + + + + rsecrelid + oid + pg_class.oid + The table this row-security is for + + + rseccmd + char + + The command this row-security is for. 'a' meaning all is only possible value right now. + + + rsecqual + pg_node_tree + + An expression tree to be performed as row-security policy + + + +
+ + + pg_class.relhasrowsecurity + must be true if a table has row-security policy in this catalog. + + +
<structname>pg_seclabel</structname> diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml new file mode 100644 index 89649a2..79f37d8 *** 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 set row-level security policy of the table. + Supplied condition performs + as if it is implicitly appended to the qualifiers of WHERE + clause, although mechanism guarantees to evaluate this condition earlier + than any other user given condition. + ALL is the only supported command type right now. + See also . + + + + + + RESET ROW SECURITY FOR rowsec_command + + + This form reset row-level security policy of the table, if exists. + 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. Expect for a case + when queries are executed with superuser privilege, only rows for which + this expression returns true will be fetched, updated or deleted. + This expression can reference columns of the relation being configured. + Sub-queries can be contained within expression tree, unless referenced + relation recursively references the same relation. + + + + diff --git a/doc/src/sgml/user-manag.sgml b/doc/src/sgml/user-manag.sgml new file mode 100644 index 177ac7a..c764189 *** a/doc/src/sgml/user-manag.sgml --- b/doc/src/sgml/user-manag.sgml *************** DROP ROLE name + + Row-security + + PostgreSQL v9.4 or later provides + row-security feature, like several commercial database + management system. It allows table owner to assign a particular + condition that performs as a security policy of the table; only + rows that satisfies the condition should be visible, except for + a case when superuser runs queries. + + + Row-security policy can be set using SET ROW SECURITY + command of statement, as an expression + form that returns a value of type boolean. This expression can contain + references to columns of the relation, so it enables to construct + arbitrary rule to make access control decision based on contents of + each rows. + + + For example, the following customer table has + uname field to store user name, and it assume + we don't want to expose any properties of other customers. + The following command set current_user = uname + as row-security policy on the customer table. + + postgres=> ALTER TABLE customer SET ROW SECURITY + FOR ALL TO (current_user = uname); + ALTER TABLE + + command shows how row-security policy + works on the supplied query. + + postgres=> EXPLAIN(costs off) SELECT * FROM customer WHERE f_leak(upasswd); + QUERY PLAN + -------------------------------------------- + Subquery Scan on customer + Filter: f_leak(customer.upasswd) + -> Seq Scan on customer customer_1 + Filter: ("current_user"() = uname) + (4 rows) + + This query execution plan means the preconfigured row-security policy is + implicitly added, and scan plan on the target relation being wrapped up + with a sub-query. + It ensures user given qualifiers, including functions with side effects, + are never executed earlier than the row-security policy regardless of + its cost, except for the cases when these were fully leakproof. + This design helps to tackle the scenario described in + ; that introduces the order to evaluate + qualifiers is significant to keep confidentiality of invisible rows. + + + + On the other hand, this design allows superusers to bypass checks with + row-security. + It ensures pg_dump can obtain a complete set + of database backup, and avoid to execute Trojan horse trap, being injected + as a row-security policy of user-defined table, with privileges of + superuser. + + + + In case of queries on inherited tables, row-security policy of the parent + relation is not applied to child relations. Scope of the row-security + policy is limited to the relation on which it is set. + + postgres=> EXPLAIN(costs off) SELECT * FROM t1 WHERE f_leak(y); + QUERY PLAN + ------------------------------------- + Append + -> Subquery Scan on t1 + Filter: f_leak(t1.y) + -> Seq Scan on t1 t1_1 + Filter: ((x % 2) = 0) + -> Seq Scan on t2 + Filter: f_leak(y) + -> Subquery Scan on t3 + Filter: f_leak(t3.y) + -> Seq Scan on t3 t3_1 + Filter: ((x % 2) = 1) + (11 rows) + + In the above example, t1 has inherited + child table t2 and t3, + and row-security policy is set on only t1, + and t3, not t2. + + The row-security policy of t1, x + must be even-number, is appended only t1, + neither t2 nor t3. + On the contrary, t3 has different row-security policy; + x must be odd-number. + + + + Row-security feature also works to queries for writer-operations; such as + , or + commands. + It ensures all the modified rows satisfies configured row-security policy. + The below query tries to update e-mail address of the + customer table, and configured row-security makes sure all the + modified rows's uname field has to match with + current_user. + + + postgres=> EXPLAIN (costs off) UPDATE customer SET email = 'alice@example.com'; + QUERY PLAN + -------------------------------------------------- + Update on customer + -> Subquery Scan on customer + -> Seq Scan on customer customer_1 + Filter: ("current_user"() = uname) + (4 rows) + + + Modification of a certain table is consist of two different stuffs; one + is fetch rows to be modified from the result relation (except for + INSERT command), second is insertion of rows to the + result relation. Usually, before-row trigger or check constraints are + run in the second phase. In addition, row-security policy is also checked + on this stage, to prevent to insert or update rows with unprivileged + values. + + + + Unlike other commercial database systems, we don't have any plan to allow + individual row-security policy for each command type. Even if we want to + perform with difference policy between and + , RETURNING clause can leak the rows + to be invisible using command. + + + + Even though it is not a specific matter in row-security, please be careful + if you plan to use current_user in row-level security policy. + and allows to + switch current user identifier during execution of the query. + Thus, it can fetch the first 100 rows with privilege of alice, + then remaining rows with privilege of bob. If and when query + execution plan contains some kind of materialization and row-security + policy contains current_user, the fetched tuples in + bob's screen might be evaluated according to the privilege of + alice. + + diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile new file mode 100644 index a974bd5..beb73df *** 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 908126c..3590ec5 *** 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 6f2e142..1ad6955 *** a/src/backend/catalog/heap.c --- b/src/backend/catalog/heap.c *************** InsertPgClassTuple(Relation pg_class_des *** 798,803 **** --- 798,804 ---- 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 9011190..e66c91b *** a/src/backend/catalog/objectaddress.c --- b/src/backend/catalog/objectaddress.c *************** getObjectDescription(const ObjectAddress *** 2143,2148 **** --- 2143,2198 ---- 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, "could not find tuple for row-security %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, "unexpected 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 ...34d33e8 *** a/src/backend/catalog/pg_rowsecurity.c --- b/src/backend/catalog/pg_rowsecurity.c *************** *** 0 **** --- 1,337 ---- + /* ------------------------------------------------------------------------- + * + * 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 on + * 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(); + { + while (HeapTupleIsValid(tuple = systable_getnext(sscan))) + { + Datum value; + bool isnull; + char *temp; + + if (!rsdesc) + { + rscxt = AllocSetContextCreate(CacheMemoryContext, + "Row-security descriptor", + ALLOCSET_SMALL_MINSIZE, + ALLOCSET_SMALL_INITSIZE, + ALLOCSET_SMALL_MAXSIZE); + oldcxt = MemoryContextSwitchTo(rscxt); + rsdesc = palloc0(sizeof(RowSecurityDesc)); + rsdesc->rscxt = rscxt; + MemoryContextSwitchTo(oldcxt); + } + 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; + } + + 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/update a row + * of 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); + + 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 0fa83a6..d657931 *** a/src/backend/commands/copy.c --- b/src/backend/commands/copy.c *************** *** 24,29 **** --- 24,30 ---- #include "access/htup_details.h" #include "access/sysattr.h" #include "access/xact.h" + #include "catalog/heap.h" #include "catalog/namespace.h" #include "catalog/pg_type.h" #include "commands/copy.h" *************** *** 34,48 **** --- 35,53 ---- #include "libpq/pqformat.h" #include "mb/pg_wchar.h" #include "miscadmin.h" + #include "nodes/makefuncs.h" #include "optimizer/clauses.h" #include "optimizer/planner.h" + #include "optimizer/rowsecurity.h" #include "parser/parse_relation.h" + #include "parser/parsetree.h" #include "rewrite/rewriteHandler.h" #include "storage/fd.h" #include "tcop/tcopprot.h" #include "utils/acl.h" #include "utils/builtins.h" #include "utils/lsyscache.h" + #include "utils/syscache.h" #include "utils/memutils.h" #include "utils/portal.h" #include "utils/rel.h" *************** DoCopy(const CopyStmt *stmt, const char *** 814,819 **** --- 819,839 ---- rel = heap_openrv(stmt->relation, (is_from ? RowExclusiveLock : AccessShareLock)); + tupDesc = RelationGetDescr(rel); + attnums = CopyGetAttnums(tupDesc, rel, stmt->attlist); + + /* + * We have to run regular query, if the target relation has + * row-level security policy + */ + if (copy_row_security_policy((CopyStmt *)stmt, rel, attnums)) + { + heap_close(rel, NoLock); /* close with keeping lock */ + relid = InvalidOid; + rel = NULL; + } + else + { relid = RelationGetRelid(rel); rte = makeNode(RangeTblEntry); *************** DoCopy(const CopyStmt *stmt, const char *** 822,829 **** rte->relkind = rel->rd_rel->relkind; rte->requiredPerms = required_access; - tupDesc = RelationGetDescr(rel); - attnums = CopyGetAttnums(tupDesc, rel, stmt->attlist); foreach(cur, attnums) { int attno = lfirst_int(cur) - --- 842,847 ---- *************** DoCopy(const CopyStmt *stmt, const char *** 835,840 **** --- 853,859 ---- rte->selectedCols = bms_add_member(rte->selectedCols, attno); } ExecCheckRTPerms(list_make1(rte), true); + } } else { *************** ProcessCopyOptions(CopyState cstate, *** 1193,1198 **** --- 1212,1264 ---- } /* + * Adjust Query tree constructed with row-level security feature. + * If WITH OIDS option was supplied, it adds Var node to reference + * object-id system column. + */ + static void + fixup_oid_of_rls_query(Query *query) + { + RangeTblEntry *subrte; + TargetEntry *subtle; + Var *subvar; + ListCell *cell; + Form_pg_attribute attform + = SystemAttributeDefinition(ObjectIdAttributeNumber, true); + + subrte = rt_fetch((Index) 1, query->rtable); + Assert(subrte->rtekind == RTE_RELATION); + + if (!SearchSysCacheExists2(ATTNUM, + ObjectIdGetDatum(subrte->relid), + Int16GetDatum(attform->attnum))) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("table \"%s\" does not have OIDs", + get_rel_name(subrte->relid)))); + + subvar = makeVar((Index) 1, + attform->attnum, + attform->atttypid, + attform->atttypmod, + attform->attcollation, + 0); + subtle = makeTargetEntry((Expr *) subvar, + 0, + pstrdup(NameStr(attform->attname)), + false); + + query->targetList = list_concat(list_make1(subtle), + query->targetList); + /* adjust resno of TargetEntry */ + foreach (cell, query->targetList) + { + subtle = lfirst(cell); + subtle->resno++; + } + } + + /* * Common setup routines used by BeginCopyFrom and BeginCopyTo. * * Iff , unload or reload in the binary format, as opposed to the *************** BeginCopy(bool is_from, *** 1264,1269 **** --- 1330,1354 ---- Assert(!is_from); cstate->rel = NULL; + /* + * In case when regular COPY TO was replaced because of row-level + * security, "raw_query" node have already analyzed / rewritten + * query tree. + */ + if (IsA(raw_query, Query)) + { + query = (Query *) raw_query; + + Assert(query->querySource == QSRC_ROW_SECURITY); + if (cstate->oids) + { + fixup_oid_of_rls_query(query); + cstate->oids = false; + } + attnamelist = NIL; + } + else + { /* Don't allow COPY w/ OIDs from a select */ if (cstate->oids) ereport(ERROR, *************** BeginCopy(bool is_from, *** 1288,1293 **** --- 1373,1379 ---- elog(ERROR, "unexpected rewrite result"); query = (Query *) linitial(rewritten); + } /* The grammar allows SELECT INTO, but we don't support that */ if (query->utilityStmt != NULL && diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c new file mode 100644 index 328e2a8..0374629 *** a/src/backend/commands/event_trigger.c --- b/src/backend/commands/event_trigger.c *************** EventTriggerSupportsObjectClass(ObjectCl *** 992,997 **** --- 992,998 ---- 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 1d9f29a..afe77c7 *** 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) *** 2789,2794 **** --- 2790,2797 ---- case AT_SetTableSpace: /* must rewrite heap */ case AT_DropNotNull: /* may change some SQL plans */ case AT_SetNotNull: + case AT_SetRowSecurity: + case AT_ResetRowSecurity: case AT_GenericOptions: case AT_AlterColumnGenericOptions: cmd_lockmode = AccessExclusiveLock; *************** ATPrepCmd(List **wqueue, Relation rel, A *** 3164,3169 **** --- 3167,3174 ---- 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 *** 3449,3454 **** --- 3454,3464 ---- 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); case AT_ReplicaIdentity: ATExecReplicaIdentity(rel, (ReplicaIdentityStmt *) cmd->def, lockmode); break; *************** ATExecAlterColumnType(AlteredTableInfo * *** 7808,7813 **** --- 7818,7839 ---- Assert(defaultexpr); break; + case OCLASS_ROWSECURITY: + /* + * A row-level security policy can depend on a column in case + * when the policy clause references a particular column. + * Due to same reason why TRIGGER ... WHEN does not support + * to change column's type being referenced in clause, row- + * level security policy also does not support it. + */ + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type of a column used in a row-level security policy"), + errdetail("%s depends on column \"%s\"", + getObjectDescription(&foundObject), + colName))); + break; + case OCLASS_PROC: case OCLASS_TYPE: case OCLASS_CAST: diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c new file mode 100644 index 6be17a9..92772bc *** a/src/backend/executor/execMain.c --- b/src/backend/executor/execMain.c *************** InitPlan(QueryDesc *queryDesc, int eflag *** 789,796 **** foreach(l, plannedstmt->rowMarks) { PlanRowMark *rc = (PlanRowMark *) lfirst(l); ! Oid relid; ! Relation relation; ExecRowMark *erm; /* ignore "parent" rowmarks; they are irrelevant at runtime */ --- 789,797 ---- foreach(l, plannedstmt->rowMarks) { PlanRowMark *rc = (PlanRowMark *) lfirst(l); ! RangeTblEntry *rte = NULL; ! Relation relation = NULL; ! LOCKMODE lockmode = NoLock; ExecRowMark *erm; /* ignore "parent" rowmarks; they are irrelevant at runtime */ *************** InitPlan(QueryDesc *queryDesc, int eflag *** 803,829 **** case ROW_MARK_NOKEYEXCLUSIVE: case ROW_MARK_SHARE: case ROW_MARK_KEYSHARE: ! relid = getrelid(rc->rti, rangeTable); ! relation = heap_open(relid, RowShareLock); break; case ROW_MARK_REFERENCE: ! relid = getrelid(rc->rti, rangeTable); ! relation = heap_open(relid, AccessShareLock); break; case ROW_MARK_COPY: /* there's no real table here ... */ - relation = NULL; break; default: elog(ERROR, "unrecognized markType: %d", rc->markType); - relation = NULL; /* keep compiler quiet */ break; } /* Check that relation is a legal target for marking */ ! if (relation) CheckValidRowMarkRel(relation, rc->markType); ! erm = (ExecRowMark *) palloc(sizeof(ExecRowMark)); erm->relation = relation; erm->rti = rc->rti; --- 804,836 ---- case ROW_MARK_NOKEYEXCLUSIVE: case ROW_MARK_SHARE: case ROW_MARK_KEYSHARE: ! rte = rt_fetch(rc->rti, rangeTable); ! lockmode = RowShareLock; break; case ROW_MARK_REFERENCE: ! rte = rt_fetch(rc->rti, rangeTable); ! lockmode = AccessShareLock; break; case ROW_MARK_COPY: /* there's no real table here ... */ break; default: elog(ERROR, "unrecognized markType: %d", rc->markType); break; } /* Check that relation is a legal target for marking */ ! if (rte) ! { ! if (rte->rtekind == RTE_RELATION) ! relation = heap_open(rte->relid, lockmode); ! else ! { ! Assert(rte->rtekind == RTE_SUBQUERY); ! relation = heap_open(rte->rowsec_relid, lockmode); ! } CheckValidRowMarkRel(relation, rc->markType); ! } erm = (ExecRowMark *) palloc(sizeof(ExecRowMark)); erm->relation = relation; erm->rti = rc->rti; diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c new file mode 100644 index e3edcf6..d5ce3d2 *** a/src/backend/nodes/copyfuncs.c --- b/src/backend/nodes/copyfuncs.c *************** _copyAppendRelInfo(const AppendRelInfo * *** 1934,1939 **** --- 1934,1940 ---- COPY_SCALAR_FIELD(parent_relid); COPY_SCALAR_FIELD(child_relid); + COPY_SCALAR_FIELD(child_result); COPY_SCALAR_FIELD(parent_reltype); COPY_SCALAR_FIELD(child_reltype); COPY_NODE_FIELD(translated_vars); *************** _copyRangeTblEntry(const RangeTblEntry * *** 1975,1980 **** --- 1976,1982 ---- COPY_SCALAR_FIELD(relkind); COPY_NODE_FIELD(subquery); COPY_SCALAR_FIELD(security_barrier); + COPY_SCALAR_FIELD(rowsec_relid); COPY_SCALAR_FIELD(jointype); COPY_NODE_FIELD(joinaliasvars); COPY_NODE_FIELD(functions); *************** _copyQuery(const Query *from) *** 2463,2468 **** --- 2465,2471 ---- COPY_SCALAR_FIELD(canSetTag); COPY_NODE_FIELD(utilityStmt); COPY_SCALAR_FIELD(resultRelation); + COPY_SCALAR_FIELD(sourceRelation); COPY_SCALAR_FIELD(hasAggs); COPY_SCALAR_FIELD(hasWindowFuncs); COPY_SCALAR_FIELD(hasSubLinks); diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c new file mode 100644 index 1f9b5d7..082d3d4 *** a/src/backend/nodes/equalfuncs.c --- b/src/backend/nodes/equalfuncs.c *************** _equalAppendRelInfo(const AppendRelInfo *** 812,817 **** --- 812,818 ---- { COMPARE_SCALAR_FIELD(parent_relid); COMPARE_SCALAR_FIELD(child_relid); + COMPARE_SCALAR_FIELD(child_result); COMPARE_SCALAR_FIELD(parent_reltype); COMPARE_SCALAR_FIELD(child_reltype); COMPARE_NODE_FIELD(translated_vars); *************** _equalQuery(const Query *a, const Query *** 847,852 **** --- 848,854 ---- COMPARE_SCALAR_FIELD(canSetTag); COMPARE_NODE_FIELD(utilityStmt); COMPARE_SCALAR_FIELD(resultRelation); + COMPARE_SCALAR_FIELD(sourceRelation); COMPARE_SCALAR_FIELD(hasAggs); COMPARE_SCALAR_FIELD(hasWindowFuncs); COMPARE_SCALAR_FIELD(hasSubLinks); *************** _equalRangeTblEntry(const RangeTblEntry *** 2245,2250 **** --- 2247,2253 ---- COMPARE_SCALAR_FIELD(relkind); COMPARE_NODE_FIELD(subquery); COMPARE_SCALAR_FIELD(security_barrier); + COMPARE_SCALAR_FIELD(rowsec_relid); COMPARE_SCALAR_FIELD(jointype); COMPARE_NODE_FIELD(joinaliasvars); COMPARE_NODE_FIELD(functions); diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c new file mode 100644 index d7db67d..b9b5aa2 *** a/src/backend/nodes/nodeFuncs.c --- b/src/backend/nodes/nodeFuncs.c *************** query_tree_walker(Query *query, *** 1941,1948 **** return true; if (walker((Node *) query->withCheckOptions, context)) return true; ! if (walker((Node *) query->returningList, context)) ! return true; if (walker((Node *) query->jointree, context)) return true; if (walker(query->setOperations, context)) --- 1941,1951 ---- return true; if (walker((Node *) query->withCheckOptions, context)) return true; ! if (!(flags & QTW_IGNORE_RETURNING)) ! { ! if (walker((Node *) query->returningList, context)) ! return true; ! } if (walker((Node *) query->jointree, context)) return true; if (walker(query->setOperations, context)) *************** query_tree_mutator(Query *query, *** 2678,2684 **** MUTATE(query->targetList, query->targetList, List *); MUTATE(query->withCheckOptions, query->withCheckOptions, List *); ! MUTATE(query->returningList, query->returningList, List *); MUTATE(query->jointree, query->jointree, FromExpr *); MUTATE(query->setOperations, query->setOperations, Node *); MUTATE(query->havingQual, query->havingQual, Node *); --- 2681,2690 ---- MUTATE(query->targetList, query->targetList, List *); MUTATE(query->withCheckOptions, query->withCheckOptions, List *); ! if (!(flags & QTW_IGNORE_RETURNING)) ! MUTATE(query->returningList, query->returningList, List *); ! else ! query->returningList = copyObject(query->returningList); MUTATE(query->jointree, query->jointree, FromExpr *); MUTATE(query->setOperations, query->setOperations, Node *); MUTATE(query->havingQual, query->havingQual, Node *); diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c new file mode 100644 index 4c7505e..fde3853 *** a/src/backend/nodes/outfuncs.c --- b/src/backend/nodes/outfuncs.c *************** _outAppendRelInfo(StringInfo str, const *** 1923,1928 **** --- 1923,1929 ---- WRITE_UINT_FIELD(parent_relid); WRITE_UINT_FIELD(child_relid); + WRITE_UINT_FIELD(child_result); WRITE_OID_FIELD(parent_reltype); WRITE_OID_FIELD(child_reltype); WRITE_NODE_FIELD(translated_vars); *************** _outQuery(StringInfo str, const Query *n *** 2238,2243 **** --- 2239,2245 ---- appendStringInfoString(str, " :utilityStmt <>"); WRITE_INT_FIELD(resultRelation); + WRITE_INT_FIELD(sourceRelation); WRITE_BOOL_FIELD(hasAggs); WRITE_BOOL_FIELD(hasWindowFuncs); WRITE_BOOL_FIELD(hasSubLinks); *************** _outRangeTblEntry(StringInfo str, const *** 2373,2378 **** --- 2375,2381 ---- case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); WRITE_BOOL_FIELD(security_barrier); + WRITE_OID_FIELD(rowsec_relid); break; case RTE_JOIN: WRITE_ENUM_FIELD(jointype, JoinType); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c new file mode 100644 index 2e2cfa7..9f32519 *** a/src/backend/nodes/readfuncs.c --- b/src/backend/nodes/readfuncs.c *************** _readQuery(void) *** 199,204 **** --- 199,205 ---- READ_BOOL_FIELD(canSetTag); READ_NODE_FIELD(utilityStmt); READ_INT_FIELD(resultRelation); + READ_INT_FIELD(sourceRelation); READ_BOOL_FIELD(hasAggs); READ_BOOL_FIELD(hasWindowFuncs); READ_BOOL_FIELD(hasSubLinks); *************** _readRangeTblEntry(void) *** 1214,1219 **** --- 1215,1221 ---- case RTE_SUBQUERY: READ_NODE_FIELD(subquery); READ_BOOL_FIELD(security_barrier); + READ_OID_FIELD(rowsec_relid); break; case RTE_JOIN: READ_ENUM_FIELD(jointype, JoinType); diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c new file mode 100644 index 6670794..8b171ce *** a/src/backend/optimizer/plan/planner.c --- b/src/backend/optimizer/plan/planner.c *************** *** 33,38 **** --- 33,39 ---- #include "optimizer/planmain.h" #include "optimizer/planner.h" #include "optimizer/prep.h" + #include "optimizer/rowsecurity.h" #include "optimizer/subselect.h" #include "optimizer/tlist.h" #include "parser/analyze.h" *************** 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,259 **** --- 256,262 ---- result->relationOids = glob->relationOids; result->invalItems = glob->invalItems; result->nParamExec = glob->nParamExec; + result->planUserId = glob->planUserId; return result; } *************** subquery_planner(PlannerGlobal *glob, Qu *** 404,409 **** --- 407,425 ---- expand_inherited_tables(root); /* + * Apply row-security policy of the relation being referenced, + * if configured with either of built-in or extension's features. + * RangeTblEntry of the relation with row-security policy shall + * be replaced with a row-security subquery that has simple scan + * on the target relation with row-security policy qualifiers. + * + * This routine assumes PlannerInfo is already handled with + * expand_inherited_tables, thus, AppendRelInfo or PlanRowMark + * have valid information. + */ + apply_row_security_policy(root); + + /* * Set hasHavingQual to remember if HAVING clause is present. Needed * because preprocess_expression will reduce a constant-true condition to * an empty qual list ... but "HAVING TRUE" is not a semantic no-op. *************** inheritance_planner(PlannerInfo *root) *** 888,893 **** --- 904,911 ---- newrti = list_length(subroot.parse->rtable) + 1; ChangeVarNodes((Node *) subroot.parse, rti, newrti, 0); ChangeVarNodes((Node *) subroot.rowMarks, rti, newrti, 0); + if (subroot.parse->sourceRelation == rti) + subroot.parse->sourceRelation = newrti; rte = copyObject(rte); subroot.parse->rtable = lappend(subroot.parse->rtable, rte); *************** inheritance_planner(PlannerInfo *root) *** 951,957 **** root->init_plans = subroot.init_plans; /* Build list of target-relation RT indexes */ ! resultRelations = lappend_int(resultRelations, appinfo->child_relid); /* Build lists of per-relation WCO and RETURNING targetlists */ if (parse->withCheckOptions) --- 969,978 ---- root->init_plans = subroot.init_plans; /* Build list of target-relation RT indexes */ ! resultRelations = lappend_int(resultRelations, ! (appinfo->child_result > 0 ? ! appinfo->child_result : ! appinfo->child_relid)); /* Build lists of per-relation WCO and RETURNING targetlists */ if (parse->withCheckOptions) diff --git a/src/backend/optimizer/prep/preptlist.c b/src/backend/optimizer/prep/preptlist.c new file mode 100644 index fb67f9e..0cccdf5 *** a/src/backend/optimizer/prep/preptlist.c --- b/src/backend/optimizer/prep/preptlist.c *************** *** 37,44 **** static List *expand_targetlist(List *tlist, int command_type, ! Index result_relation, List *range_table); /* * preprocess_targetlist --- 37,85 ---- static List *expand_targetlist(List *tlist, int command_type, ! Index result_relation, Index source_relation, ! List *range_table); ! ! /* ! * lookup_varattno ! * ! * This routine returns an attribute number to reference a particular ! * attribute. In case when the target relation is really relation, ! * we can reference arbitrary attribute (including system column) ! * without any translations. However, we have to translate varattno ! * of Var that references sub-queries being originated from regular ! * relations with row-level security policy due to nature of sub-query ! * that has no system-column. ! */ ! static AttrNumber ! lookup_varattno(AttrNumber attno, Index rt_index, List *rtables) ! { ! RangeTblEntry *rte = rt_fetch(rt_index, rtables); ! ! if (rte->rtekind == RTE_SUBQUERY && ! rte->subquery->querySource == QSRC_ROW_SECURITY) ! { ! ListCell *cell; ! ! foreach (cell, rte->subquery->targetList) ! { ! TargetEntry *tle = lfirst(cell); ! Var *var; ! ! if (IsA(tle->expr, Const)) ! continue; ! ! var = (Var *) tle->expr; ! Assert(IsA(var, Var)); + if (var->varattno == attno) + return tle->resno; + } + elog(ERROR, "invalid attno %d on row-security subquery target-list", + attno); + } + return attno; + } /* * preprocess_targetlist *************** preprocess_targetlist(PlannerInfo *root, *** 51,56 **** --- 92,98 ---- { Query *parse = root->parse; int result_relation = parse->resultRelation; + int source_relation = parse->sourceRelation; List *range_table = parse->rtable; CmdType command_type = parse->commandType; ListCell *lc; *************** preprocess_targetlist(PlannerInfo *root, *** 73,80 **** * 10/94 */ if (command_type == CMD_INSERT || command_type == CMD_UPDATE) tlist = expand_targetlist(tlist, command_type, ! result_relation, range_table); /* * Add necessary junk columns for rowmarked rels. These values are needed --- 115,126 ---- * 10/94 */ if (command_type == CMD_INSERT || command_type == CMD_UPDATE) + { tlist = expand_targetlist(tlist, command_type, ! result_relation, ! source_relation, ! range_table); ! } /* * Add necessary junk columns for rowmarked rels. These values are needed *************** preprocess_targetlist(PlannerInfo *root, *** 96,102 **** { /* It's a regular table, so fetch its TID */ var = makeVar(rc->rti, ! SelfItemPointerAttributeNumber, TIDOID, -1, InvalidOid, --- 142,149 ---- { /* It's a regular table, so fetch its TID */ var = makeVar(rc->rti, ! lookup_varattno(SelfItemPointerAttributeNumber, ! rc->rti, range_table), TIDOID, -1, InvalidOid, *************** preprocess_targetlist(PlannerInfo *root, *** 112,118 **** if (rc->isParent) { var = makeVar(rc->rti, ! TableOidAttributeNumber, OIDOID, -1, InvalidOid, --- 159,166 ---- if (rc->isParent) { var = makeVar(rc->rti, ! lookup_varattno(TableOidAttributeNumber, ! rc->rti, range_table), OIDOID, -1, InvalidOid, *************** preprocess_targetlist(PlannerInfo *root, *** 195,201 **** */ static List * expand_targetlist(List *tlist, int command_type, ! Index result_relation, List *range_table) { List *new_tlist = NIL; ListCell *tlist_item; --- 243,250 ---- */ static List * expand_targetlist(List *tlist, int command_type, ! Index result_relation, Index source_relation, ! List *range_table) { List *new_tlist = NIL; ListCell *tlist_item; *************** expand_targetlist(List *tlist, int comma *** 218,223 **** --- 267,275 ---- numattrs = RelationGetNumberOfAttributes(rel); + if (source_relation == 0) + source_relation = result_relation; + for (attrno = 1; attrno <= numattrs; attrno++) { Form_pg_attribute att_tup = rel->rd_att->attrs[attrno - 1]; *************** expand_targetlist(List *tlist, int comma *** 298,305 **** case CMD_UPDATE: if (!att_tup->attisdropped) { ! new_expr = (Node *) makeVar(result_relation, ! attrno, atttype, atttypmod, attcollation, --- 350,359 ---- case CMD_UPDATE: if (!att_tup->attisdropped) { ! new_expr = (Node *) makeVar(source_relation, ! lookup_varattno(attrno, ! source_relation, ! range_table), atttype, atttypmod, attcollation, diff --git a/src/backend/optimizer/prep/prepunion.c b/src/backend/optimizer/prep/prepunion.c new file mode 100644 index e249628..017bc2c *** a/src/backend/optimizer/prep/prepunion.c --- b/src/backend/optimizer/prep/prepunion.c *************** typedef struct *** 55,60 **** --- 55,61 ---- { PlannerInfo *root; AppendRelInfo *appinfo; + bool in_returning; } adjust_appendrel_attrs_context; static Plan *recurse_set_operations(Node *setOp, PlannerInfo *root, *************** adjust_appendrel_attrs(PlannerInfo *root *** 1594,1599 **** --- 1595,1601 ---- context.root = root; context.appinfo = appinfo; + context.in_returning = false; /* * Must be prepared to start with a Query or a bare expression tree. *************** adjust_appendrel_attrs(PlannerInfo *root *** 1605,1614 **** newnode = query_tree_mutator((Query *) node, adjust_appendrel_attrs_mutator, (void *) &context, ! QTW_IGNORE_RC_SUBQUERIES); if (newnode->resultRelation == appinfo->parent_relid) { ! newnode->resultRelation = appinfo->child_relid; /* Fix tlist resnos too, if it's inherited UPDATE */ if (newnode->commandType == CMD_UPDATE) newnode->targetList = --- 1607,1635 ---- newnode = query_tree_mutator((Query *) node, adjust_appendrel_attrs_mutator, (void *) &context, ! QTW_IGNORE_RC_SUBQUERIES | ! QTW_IGNORE_RETURNING); ! /* ! * Returning clause on the relation being replaced with row- ! * security subquery shall be handled in a special way, because ! * of no system columns on subquery. ! * Var references to system column or whole-row reference need ! * to be adjusted to reference pseudo columns on behalf of ! * the underlying these columns, however, RETURNGIN clause is ! * an exception because its Var nodes are evaluated towards ! * the "raw" target relation, not a fetched tuple. ! */ ! context.in_returning = true; ! newnode->returningList = (List *) ! expression_tree_mutator((Node *) newnode->returningList, ! adjust_appendrel_attrs_mutator, ! (void *) &context); if (newnode->resultRelation == appinfo->parent_relid) { ! newnode->resultRelation = (appinfo->child_result > 0 ? ! appinfo->child_result : ! appinfo->child_relid); ! newnode->sourceRelation = appinfo->child_relid; /* Fix tlist resnos too, if it's inherited UPDATE */ if (newnode->commandType == CMD_UPDATE) newnode->targetList = *************** adjust_appendrel_attrs(PlannerInfo *root *** 1624,1629 **** --- 1645,1693 ---- } static Node * + fixup_var_on_rowsec_subquery(RangeTblEntry *rte, Var *var) + { + ListCell *cell; + + Assert(rte->rtekind == RTE_SUBQUERY && + rte->subquery->querySource == QSRC_ROW_SECURITY); + /* + * In case when row-level security policy is applied on the referenced + * table, its RangeTblEntry (RTE_RELATION) is replaced with sub-query + * to filter out unprivileged rows of underlying relation. + * Even though reference to this sub-query should perform as if ones + * to real relations, system column has to be cared in special way + * due to the nature of sub-query. + * Target-entries that reference system columns should be added on + * rowlevelsec.c, so all we need to do here is looking up underlying + * target-list that can reference underlying system column, and fix- + * up varattno of the referencing Var node with resno of TargetEntry. + */ + foreach (cell, rte->subquery->targetList) + { + TargetEntry *subtle = lfirst(cell); + + if (IsA(subtle->expr, Var)) + { + Var *subvar = (Var *) subtle->expr; + Var *newnode; + + if (subvar->varattno == var->varattno) + { + newnode = copyObject(var); + newnode->varattno = subtle->resno; + return (Node *)newnode; + } + } + else + Assert(IsA(subtle->expr, Const)); + } + elog(ERROR, "could not find pseudo column of %d, relid %u", + var->varattno, var->varno); + return NULL; + } + + static Node * adjust_appendrel_attrs_mutator(Node *node, adjust_appendrel_attrs_context *context) { *************** adjust_appendrel_attrs_mutator(Node *nod *** 1638,1645 **** if (var->varlevelsup == 0 && var->varno == appinfo->parent_relid) { ! var->varno = appinfo->child_relid; var->varnoold = appinfo->child_relid; if (var->varattno > 0) { Node *newnode; --- 1702,1713 ---- if (var->varlevelsup == 0 && var->varno == appinfo->parent_relid) { ! var->varno = (context->in_returning && ! appinfo->child_result > 0 ? ! appinfo->child_result : ! appinfo->child_relid); var->varnoold = appinfo->child_relid; + if (var->varattno > 0) { Node *newnode; *************** adjust_appendrel_attrs_mutator(Node *nod *** 1664,1669 **** --- 1732,1745 ---- */ if (OidIsValid(appinfo->child_reltype)) { + Query *parse = context->root->parse; + RangeTblEntry *rte = rt_fetch(appinfo->child_relid, + parse->rtable); + if (!context->in_returning && + rte->rtekind == RTE_SUBQUERY && + rte->subquery->querySource == QSRC_ROW_SECURITY) + var = (Var *)fixup_var_on_rowsec_subquery(rte, var); + Assert(var->vartype == appinfo->parent_reltype); if (appinfo->parent_reltype != appinfo->child_reltype) { *************** adjust_appendrel_attrs_mutator(Node *nod *** 1708,1714 **** return (Node *) rowexpr; } } ! /* system attributes don't need any other translation */ } return (Node *) var; } --- 1784,1801 ---- return (Node *) rowexpr; } } ! else ! { ! Query *parse = context->root->parse; ! RangeTblEntry *rte; ! ! rte = rt_fetch(appinfo->child_relid, parse->rtable); ! ! if (!context->in_returning && ! rte->rtekind == RTE_SUBQUERY && ! rte->subquery->querySource == QSRC_ROW_SECURITY) ! return fixup_var_on_rowsec_subquery(rte, var); ! } } return (Node *) var; } diff --git a/src/backend/optimizer/util/Makefile b/src/backend/optimizer/util/Makefile new file mode 100644 index 3b2d16b..3f5cb19 *** a/src/backend/optimizer/util/Makefile --- b/src/backend/optimizer/util/Makefile *************** top_builddir = ../../../.. *** 13,18 **** include $(top_builddir)/src/Makefile.global OBJS = clauses.o joininfo.o pathnode.o placeholder.o plancat.o predtest.o \ ! relnode.o restrictinfo.o tlist.o var.o include $(top_srcdir)/src/backend/common.mk --- 13,18 ---- include $(top_builddir)/src/Makefile.global OBJS = clauses.o joininfo.o pathnode.o placeholder.o plancat.o predtest.o \ ! relnode.o restrictinfo.o tlist.o var.o rowsecurity.o include $(top_srcdir)/src/backend/common.mk diff --git a/src/backend/optimizer/util/rowsecurity.c b/src/backend/optimizer/util/rowsecurity.c new file mode 100644 index ...c2e5a49 *** a/src/backend/optimizer/util/rowsecurity.c --- b/src/backend/optimizer/util/rowsecurity.c *************** *** 0 **** --- 1,744 ---- + /* + * optimizer/util/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 "optimizer/clauses.h" + #include "optimizer/prep.h" + #include "optimizer/rowsecurity.h" + #include "parser/parsetree.h" + #include "rewrite/rewriteHandler.h" + #include "utils/lsyscache.h" + #include "utils/rel.h" + #include "utils/syscache.h" + #include "tcop/utility.h" + + /* flags to pull row-security policy */ + #define RSEC_FLAG_HAS_SUBLINKS 0x0001 + + /* hook to allow extensions to apply their own security policy */ + row_security_policy_hook_type row_security_policy_hook = NULL; + + /* + * make_pseudo_column + * + * It makes a target-entry node that references underlying column. + * Its tle->expr is usualy Var node, but may be Const for dummy NULL + * if the supplied attribute was already dropped. + */ + static TargetEntry * + make_pseudo_column(RangeTblEntry *subrte, AttrNumber attnum) + { + Expr *expr; + char *resname; + + Assert(subrte->rtekind == RTE_RELATION && OidIsValid(subrte->relid)); + if (attnum == InvalidAttrNumber) + { + expr = (Expr *) makeWholeRowVar(subrte, (Index) 1, 0, false); + resname = get_rel_name(subrte->relid); + } + else + { + HeapTuple tuple; + Form_pg_attribute attform; + + tuple = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(subrte->relid), + Int16GetDatum(attnum)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + attnum, subrte->relid); + attform = (Form_pg_attribute) GETSTRUCT(tuple); + + if (attform->attisdropped) + { + char namebuf[NAMEDATALEN]; + + /* Insert NULL just for a placeholder of dropped column */ + expr = (Expr *) makeConst(INT4OID, + -1, + InvalidOid, + sizeof(int32), + (Datum) 0, + true, /* isnull */ + true); /* byval */ + sprintf(namebuf, "dummy-%d", (int)attform->attnum); + resname = pstrdup(namebuf); + } + else + { + expr = (Expr *) makeVar((Index) 1, + attform->attnum, + attform->atttypid, + attform->atttypmod, + attform->attcollation, + 0); + resname = pstrdup(NameStr(attform->attname)); + } + ReleaseSysCache(tuple); + } + return makeTargetEntry(expr, -1, resname, false); + } + + /* + * lookup_pseudo_column + * + * It looks-up resource number of the target-entry relevant to the given + * Var-node that references the row-security subquery. If required column + * is not in the subquery's target-list, this function also adds new one + * and returns its resource number. + */ + static AttrNumber + lookup_pseudo_column(PlannerInfo *root, + RangeTblEntry *rte, AttrNumber varattno) + { + Query *subqry; + RangeTblEntry *subrte; + TargetEntry *subtle; + ListCell *cell; + + Assert(rte->rtekind == RTE_SUBQUERY && + rte->subquery->querySource == QSRC_ROW_SECURITY); + + subqry = rte->subquery; + foreach (cell, subqry->targetList) + { + subtle = lfirst(cell); + + /* + * If referenced artifical column is already constructed on the + * target-list of row-security subquery, nothing to do any more. + */ + if (IsA(subtle->expr, Var)) + { + Var *subvar = (Var *)subtle->expr; + + Assert(subvar->varno == 1); + if (subvar->varattno == varattno) + return subtle->resno; + } + } + + /* + * OK, we don't have an artifical column relevant to the required ones, + * so let's create a new artifical column on demand. + */ + subrte = rt_fetch((Index) 1, subqry->rtable); + subtle = make_pseudo_column(subrte, varattno); + subtle->resno = list_length(subqry->targetList) + 1; + + subqry->targetList = lappend(subqry->targetList, subtle); + rte->eref->colnames = lappend(rte->eref->colnames, + makeString(pstrdup(subtle->resname))); + return subtle->resno; + } + + /* + * fixup_varnode_walker + * + * It recursively fixes up references to the relation to be replaced by + * row-security sub-query, and adds pseudo columns relevant to the + * underlying system columns or whole row-reference on demand. + */ + typedef struct { + PlannerInfo *root; + int varlevelsup; + Index *vartrans; + } fixup_varnode_context; + + static bool + fixup_varnode_walker(Node *node, fixup_varnode_context *context) + { + if (node == NULL) + return false; + + if (IsA(node, Var)) + { + Var *var = (Var *) node; + List *rtable = context->root->parse->rtable; + RangeTblEntry *rte; + ListCell *cell; + + /* + * Ignore it, if Var node does not reference the Query currently + * we focus on. + */ + if (var->varlevelsup != context->varlevelsup) + return false; + + if (context->vartrans[var->varno] > 0) + { + Index rtindex_trans = context->vartrans[var->varno]; + + rte = rt_fetch(rtindex_trans, rtable); + Assert(rte->rtekind == RTE_SUBQUERY && + rte->subquery->querySource == QSRC_ROW_SECURITY); + + var->varno = var->varnoold = rtindex_trans; + var->varattno = lookup_pseudo_column(context->root, rte, + var->varattno); + } + else + { + rte = rt_fetch(var->varno, rtable); + if (rte->rtekind == RTE_RELATION && rte->inh) + { + foreach (cell, context->root->append_rel_list) + { + AppendRelInfo *appinfo = lfirst(cell); + RangeTblEntry *child_rte; + + if (appinfo->parent_relid != var->varno) + continue; + + child_rte = rt_fetch(appinfo->child_relid, rtable); + if (child_rte->rtekind == RTE_SUBQUERY && + child_rte->subquery->querySource == QSRC_ROW_SECURITY) + (void) lookup_pseudo_column(context->root, + child_rte, + var->varattno); + } + } + } + } + else if (IsA(node, RangeTblRef)) + { + RangeTblRef *rtr = (RangeTblRef *) node; + + if (context->varlevelsup == 0 && + context->vartrans[rtr->rtindex] != 0) + rtr->rtindex = context->vartrans[rtr->rtindex]; + } + else if (IsA(node, Query)) + { + bool result; + + context->varlevelsup++; + result = query_tree_walker((Query *) node, + fixup_varnode_walker, + (void *) context, 0); + context->varlevelsup--; + + return result; + } + return expression_tree_walker(node, + fixup_varnode_walker, + (void *) context); + } + + /* + * check_infinite_recursion + * + * It is a wrong row-security configuration, if we try to expand + * the relation inside of row-security subquery originated from + * same relation! + */ + static void + check_infinite_recursion(PlannerInfo *root, Oid relid) + { + PlannerInfo *parent = root->parent_root; + + if (parent && parent->parse->querySource == QSRC_ROW_SECURITY) + { + RangeTblEntry *rte = rt_fetch(1, parent->parse->rtable); + + Assert(rte->rtekind == RTE_RELATION && OidIsValid(rte->relid)); + + if (relid == rte->relid) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("infinite recursion detected for relation \"%s\"", + get_rel_name(relid)))); + check_infinite_recursion(parent, relid); + } + } + + /* + * expand_rtentry_with_policy + * + * It extends a range-table entry of row-security sub-query with supplied + * security policy, and append it on the parse->rtable. + * This sub-query contains pseudo columns that reference underlying + * regular columns (at least, references to system column or whole of + * table reference shall be added on demand), and simple scan on the + * target relation. + * Any Var nodes that referenced the relation pointed by rtindex shall + * be adjusted to reference this sub-query instead. walker + */ + static Index + expand_rtentry_with_policy(PlannerInfo *root, Index rtindex, + Expr *qual, int flags) + { + Query *parse = root->parse; + RangeTblEntry *rte = rt_fetch(rtindex, parse->rtable); + Query *subqry; + RangeTblEntry *subrte; + RangeTblRef *subrtr; + TargetEntry *subtle; + RangeTblEntry *newrte; + HeapTuple tuple; + AttrNumber nattrs; + AttrNumber attnum; + List *targetList = NIL; + List *colNameList = NIL; + PlanRowMark *rowmark; + + Assert(rte->rtekind == RTE_RELATION && !rte->inh); + + /* check recursion to prevent infinite loop */ + check_infinite_recursion(root, rte->relid); + + /* Expand views inside SubLink node */ + if (flags & RSEC_FLAG_HAS_SUBLINKS) + QueryRewriteExpr((Node *)qual, list_make1_oid(rte->relid)); + + /* + * Construction of sub-query + */ + subqry = (Query *) makeNode(Query); + subqry->commandType = CMD_SELECT; + subqry->querySource = QSRC_ROW_SECURITY; + + subrte = copyObject(rte); + subqry->rtable = list_make1(subrte); + + subrtr = makeNode(RangeTblRef); + subrtr->rtindex = 1; + subqry->jointree = makeFromExpr(list_make1(subrtr), (Node *) qual); + if (flags & RSEC_FLAG_HAS_SUBLINKS) + subqry->hasSubLinks = true; + + /* + * Construction of TargetEntries that reference underlying columns. + */ + tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(rte->relid)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for relation %u", rte->relid); + nattrs = ((Form_pg_class) GETSTRUCT(tuple))->relnatts; + ReleaseSysCache(tuple); + + for (attnum = 1; attnum <= nattrs; attnum++) + { + subtle = make_pseudo_column(subrte, attnum); + subtle->resno = list_length(targetList) + 1; + Assert(subtle->resno == attnum); + + targetList = lappend(targetList, subtle); + colNameList = lappend(colNameList, + makeString(pstrdup(subtle->resname))); + } + subqry->targetList = targetList; + + /* Expand RengeTblEntry with this sub-query */ + newrte = makeNode(RangeTblEntry); + newrte->rtekind = RTE_SUBQUERY; + newrte->subquery = subqry; + newrte->security_barrier = true; + newrte->rowsec_relid = rte->relid; + newrte->eref = makeAlias(get_rel_name(rte->relid), colNameList); + + parse->rtable = lappend(parse->rtable, newrte); + + /* + * Fix up PlanRowMark if needed, then add references to 'tableoid' and + * 'ctid' that shall be added to handle row-level locking. + * Also see preprocess_targetlist() that adds some junk attributes. + */ + rowmark = get_plan_rowmark(root->rowMarks, rtindex); + if (rowmark) + { + if (rowmark->rti == rowmark->prti) + rowmark->rti = rowmark->prti = list_length(parse->rtable); + else + rowmark->rti = list_length(parse->rtable); + + lookup_pseudo_column(root, newrte, SelfItemPointerAttributeNumber); + lookup_pseudo_column(root, newrte, TableOidAttributeNumber); + } + return list_length(parse->rtable); + } + + /* + * pull_row_security_policy + * + * It pulls the configured row-security policy of both built-in and + * extensions. If any, it returns expression tree. + */ + static Expr * + pull_row_security_policy(CmdType cmd, Relation relation, int *p_flags) + { + Expr *quals = NULL; + int flags = 0; + + /* + * Pull the row-security policy configured with built-in features, + * if unprivileged users. Please note that superuser can bypass it. + */ + if (relation->rsdesc && !superuser()) + { + RowSecurityDesc *rsdesc = relation->rsdesc; + + quals = copyObject(rsdesc->rsall.qual); + if (rsdesc->rsall.hassublinks) + flags |= RSEC_FLAG_HAS_SUBLINKS; + } + + /* + * Also, ask extensions whether they want to apply their own + * row-security policy. If both built-in and extension has + * their own policy, it shall be merged. + */ + if (row_security_policy_hook) + { + List *temp; + + temp = (*row_security_policy_hook)(cmd, relation); + if (temp != NIL) + { + if ((flags & RSEC_FLAG_HAS_SUBLINKS) == 0 && + contain_subplans((Node *) temp)) + flags |= RSEC_FLAG_HAS_SUBLINKS; + + if (quals != NULL) + temp = lappend(temp, quals); + + if (list_length(temp) == 1) + quals = (Expr *)list_head(temp); + else if (list_length(temp) > 1) + quals = makeBoolExpr(AND_EXPR, temp, -1); + } + } + *p_flags = flags; + return quals; + } + + /* + * copy_row_security_policy + * + * It construct a row-security subquery instead of raw COPY TO statement, + * if target relation has a row-level security policy + */ + bool + copy_row_security_policy(CopyStmt *stmt, Relation rel, List *attnums) + { + Expr *quals; + int flags; + Query *parse; + RangeTblEntry *rte; + RangeTblRef *rtr; + TargetEntry *tle; + Var *var; + ListCell *cell; + + if (stmt->is_from) + return false; + + quals = pull_row_security_policy(CMD_SELECT, rel, &flags); + if (!quals) + return false; + + parse = (Query *) makeNode(Query); + parse->commandType = CMD_SELECT; + parse->querySource = QSRC_ROW_SECURITY; + + rte = makeNode(RangeTblEntry); + rte->rtekind = RTE_RELATION; + rte->relid = RelationGetRelid(rel); + rte->relkind = RelationGetForm(rel)->relkind; + + foreach (cell, attnums) + { + HeapTuple tuple; + Form_pg_attribute attform; + AttrNumber attno = lfirst_int(cell); + + tuple = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(RelationGetRelid(rel)), + Int16GetDatum(attno)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for attribute %d of relation %s", + attno, RelationGetRelationName(rel)); + attform = (Form_pg_attribute) GETSTRUCT(tuple); + + var = makeVar((Index) 1, + attform->attnum, + attform->atttypid, + attform->atttypmod, + attform->attcollation, + 0); + tle = makeTargetEntry((Expr *) var, + list_length(parse->targetList) + 1, + pstrdup(NameStr(attform->attname)), + false); + parse->targetList = lappend(parse->targetList, tle); + + ReleaseSysCache(tuple); + + rte->selectedCols = bms_add_member(rte->selectedCols, + attno - FirstLowInvalidHeapAttributeNumber); + } + rte->inFromCl = true; + rte->requiredPerms = ACL_SELECT; + + rtr = makeNode(RangeTblRef); + rtr->rtindex = 1; + + parse->jointree = makeFromExpr(list_make1(rtr), (Node *) quals); + parse->rtable = list_make1(rte); + if (flags & RSEC_FLAG_HAS_SUBLINKS) + parse->hasSubLinks = true; + + stmt->query = (Node *) parse; + + return true; + } + + /* + * apply_row_security_relation + * + * It applies row-security policy on a particular relation being specified. + * If this relation is top of the inheritance tree, it also checks inherited + * children. + */ + static bool + apply_row_security_relation(PlannerInfo *root, Index *vartrans, + CmdType cmd, Index rtindex) + { + Query *parse = root->parse; + RangeTblEntry *rte = rt_fetch(rtindex, parse->rtable); + Relation rel; + Expr *qual; + int flags; + bool result = false; + + if (!rte->inh) + { + rel = heap_open(rte->relid, NoLock); + qual = pull_row_security_policy(cmd, rel, &flags); + if (qual) + { + vartrans[rtindex] + = expand_rtentry_with_policy(root, rtindex, qual, flags); + if (parse->resultRelation == rtindex) + parse->sourceRelation = vartrans[rtindex]; + result = true; + } + heap_close(rel, NoLock); + } + else + { + /* + * In case when relation has inherited children, we try to apply + * row-level security policy of them if configured. + * In addition to regular replacement with a sub-query, we need + * to adjust rtindex of AppendRelInfo and varno of translated_vars. + * It makes sub-queries perform like regular relations being + * inherited from a particular parent relation. So, a table scan + * may have underlying a relation scan and two sub-query scans for + * instance. If it is result relation of UPDATE or DELETE command, + * rtindex to the original relation (regular relation) has to be + * kept because sub-query cannot perform as an updatable relation. + * So, we save it on child_result of AppendRelInfo; that shall be + * used to track relations to be modified at inheritance_planner(). + */ + ListCell *lc1, *lc2; + + foreach (lc1, root->append_rel_list) + { + AppendRelInfo *apinfo = lfirst(lc1); + + if (apinfo->parent_relid != rtindex) + continue; + + if (apply_row_security_relation(root, vartrans, cmd, + apinfo->child_relid)) + { + /* + * Save the rtindex of actual relation to be modified, + * if parent relation is result relation of this query. + */ + if (parse->resultRelation == rtindex) + apinfo->child_result = apinfo->child_relid; + + apinfo->child_relid = vartrans[apinfo->child_relid]; + /* Adjust varno to reference pseudo columns */ + foreach (lc2, apinfo->translated_vars) + { + Var *var = lfirst(lc2); + + if (var) + var->varno = apinfo->child_relid; + } + result = true; + } + } + } + return result; + } + + /* + * apply_row_security_recursive + * + * It walks on the given join-tree to replace relations with row-level + * security policy by a simple sub-query. + */ + static bool + apply_row_security_recursive(PlannerInfo *root, Index *vartrans, Node *jtnode) + { + bool result = false; + + if (jtnode == NULL) + return false; + if (IsA(jtnode, RangeTblRef)) + { + Index rtindex = ((RangeTblRef *) jtnode)->rtindex; + Query *parse = root->parse; + RangeTblEntry *rte = rt_fetch(rtindex, parse->rtable); + CmdType cmd; + + /* Only relation can have row-security policy */ + if (rte->rtekind != RTE_RELATION) + return false; + + /* + * Prevents infinite recursion. Please note that rtindex == 1 + * of the row-security subquery is a relation being already + * processed on the upper level. + */ + if (parse->querySource == QSRC_ROW_SECURITY && rtindex == 1) + return false; + + /* Is it a result relation of UPDATE or DELETE command? */ + if (parse->resultRelation == rtindex) + cmd = parse->commandType; + else + cmd = CMD_SELECT; + + /* Try to apply row-security policy, if configured */ + result = apply_row_security_relation(root, vartrans, cmd, rtindex); + } + else if (IsA(jtnode, FromExpr)) + { + FromExpr *f = (FromExpr *) jtnode; + ListCell *l; + + foreach (l, f->fromlist) + { + if (apply_row_security_recursive(root, vartrans, lfirst(l))) + result = true; + } + } + else if (IsA(jtnode, JoinExpr)) + { + JoinExpr *j = (JoinExpr *) jtnode; + + if (apply_row_security_recursive(root, vartrans, j->larg)) + result = true; + if (apply_row_security_recursive(root, vartrans, j->rarg)) + result = true; + } + else + elog(ERROR, "unexpected node type: %d", (int) nodeTag(jtnode)); + + return result; + } + + /* + * apply_row_security_policy + * + * Entrypoint to apply configured row-security policy of the relation. + * + * In case when the supplied query references relations with row-security + * policy, its RangeTblEntry shall be replaced by a row-security subquery + * that has simple scan on the referenced table with policy qualifiers. + * Of course, security-barrier shall be set on the subquery to prevent + * unexpected push-down of functions without leakproof flag. + * + * For example, when table t1 has a security policy "(x % 2 = 0)", the + * following query: + * SELECT * FROM t1 WHERE f_leak(y) + * performs as if + * SELECT * FROM ( + * SELECT x, y FROM t1 WHERE (x % 2 = 0) + * ) AS t1 WHERE f_leak(y) + * would be given. Because the sub-query has security barrier flag, + * configured security policy qualifier is always executed prior to + * user given functions. + */ + void + apply_row_security_policy(PlannerInfo *root) + { + Query *parse = root->parse; + Oid curr_userid; + int curr_seccxt; + Index *vartrans; + + /* + * Mode checks. In case when SECURITY_ROW_LEVEL_DISABLED is set, + * no row-level security policy should be applied regardless + * whether it is built-in or extension. + */ + GetUserIdAndSecContext(&curr_userid, &curr_seccxt); + if (curr_seccxt & SECURITY_ROW_LEVEL_DISABLED) + return; + + vartrans = palloc0(sizeof(Index) * (list_length(parse->rtable) + 1)); + if (apply_row_security_recursive(root, vartrans, (Node *)parse->jointree)) + { + PlannerGlobal *glob = root->glob; + PlanInvalItem *pi_item; + fixup_varnode_context context; + + /* + * Constructed Plan with row-level security policy depends on + * properties of current user (database superuser can bypass + * configured row-security policy!), thus, it has to be + * invalidated when its assumption was changed. + */ + if (!OidIsValid(glob->planUserId)) + { + /* Plan invalidation on session user-id */ + glob->planUserId = GetUserId(); + + /* Plan invalidation on catalog updates of pg_authid */ + pi_item = makeNode(PlanInvalItem); + pi_item->cacheId = AUTHOID; + pi_item->hashValue = + GetSysCacheHashValue1(AUTHOID, + ObjectIdGetDatum(glob->planUserId)); + glob->invalItems = lappend(glob->invalItems, pi_item); + } + else + Assert(glob->planUserId == GetUserId()); + + /* + * Var-nodes that referenced RangeTblEntry to be replaced by + * row-security sub-query have to be adjusted for appropriate + * reference to the underlying pseudo column of the relation. + */ + context.root = root; + context.varlevelsup = 0; + context.vartrans = vartrans; + query_tree_walker(parse, + fixup_varnode_walker, + (void *) &context, + QTW_IGNORE_RETURNING); + } + pfree(vartrans); + } diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y new file mode 100644 index 1922097..2c22de8 *** a/src/backend/parser/gram.y --- b/src/backend/parser/gram.y *************** static Node *makeRecursiveViewSelect(cha *** 257,262 **** --- 257,263 ---- %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: *** 2183,2188 **** --- 2184,2206 ---- 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; + } /* ALTER TABLE REPLICA IDENTITY */ | REPLICA IDENTITY_P replica_identity { *************** reloption_elem: *** 2293,2298 **** --- 2311,2322 ---- } ; + 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 98cb58a..ac50a2e *** a/src/backend/parser/parse_agg.c --- b/src/backend/parser/parse_agg.c *************** transformAggregateCall(ParseState *pstat *** 272,277 **** --- 272,280 ---- 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 *** 547,552 **** --- 550,558 ---- 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 68b711d..fb09c8c *** a/src/backend/parser/parse_expr.c --- b/src/backend/parser/parse_expr.c *************** transformSubLink(ParseState *pstate, Sub *** 1459,1464 **** --- 1459,1465 ---- 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 *** 2640,2645 **** --- 2641,2648 ---- 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/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c new file mode 100644 index 50cb753..f549293 *** a/src/backend/rewrite/rewriteHandler.c --- b/src/backend/rewrite/rewriteHandler.c *************** QueryRewrite(Query *parsetree) *** 3282,3284 **** --- 3282,3300 ---- return results; } + + /* + * QueryRewriteExpr + * + * This routine provides an entry point of query rewriter towards + * a certain expression tree with SubLink node; being added after + * the top level query rewrite. + * It primarily intends to expand views appeared in the qualifiers + * appended with row-level security which needs to modify query + * tree at head of the planner stage. + */ + void + QueryRewriteExpr(Node *node, List *activeRIRs) + { + fireRIRonSubLink(node, activeRIRs); + } diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c new file mode 100644 index 917130f..b24af71 *** a/src/backend/utils/adt/ri_triggers.c --- b/src/backend/utils/adt/ri_triggers.c *************** ri_PerformCheck(const RI_ConstraintInfo *** 3008,3013 **** --- 3008,3014 ---- int spi_result; Oid save_userid; int save_sec_context; + int temp_sec_context; Datum vals[RI_MAX_NUMKEYS * 2]; char nulls[RI_MAX_NUMKEYS * 2]; *************** ri_PerformCheck(const RI_ConstraintInfo *** 3087,3094 **** /* 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); /* Finally we can run the query. */ spi_result = SPI_execute_snapshot(qplan, --- 3088,3105 ---- /* Switch to proper UID to perform check as */ GetUserIdAndSecContext(&save_userid, &save_sec_context); + + /* + * Row-level security should be disabled in case when foreign-key + * relation is queried to check existence of tuples that references + * the primary-key being modified. + */ + temp_sec_context = save_sec_context | SECURITY_LOCAL_USERID_CHANGE; + if (source_is_pk) + temp_sec_context |= SECURITY_ROW_LEVEL_DISABLED; + SetUserIdAndSecContext(RelationGetForm(query_rel)->relowner, ! temp_sec_context); /* Finally we can run the query. */ spi_result = SPI_execute_snapshot(qplan, diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c new file mode 100644 index cf740a9..f1e67a5 *** 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" *************** CheckCachedPlan(CachedPlanSource *planso *** 795,800 **** --- 796,811 ---- AcquireExecutorLocks(plan->stmt_list, true); /* + * If plan was constructed with assumption of a particular user-id, + * and it is different from the current one, the cached-plan shall + * be invalidated to construct suitable query plan. + */ + if (plan->is_valid && + OidIsValid(plan->planUserId) && + plan->planUserId == GetUserId()) + plan->is_valid = false; + + /* * If plan was transient, check to see if TransactionXmin has * advanced, and if so invalidate it. */ *************** BuildCachedPlan(CachedPlanSource *planso *** 847,852 **** --- 858,865 ---- { CachedPlan *plan; List *plist; + ListCell *cell; + Oid planUserId = InvalidOid; bool snapshot_set; bool spi_pushed; MemoryContext plan_context; *************** BuildCachedPlan(CachedPlanSource *planso *** 914,919 **** --- 927,950 ---- PopActiveSnapshot(); /* + * Check whether the generated plan assumes a particular user-id, or not. + * In case when a valid user-id is recorded on PlannedStmt->planUserId, + * it should be kept and used to validation check of the cached plan + * under the "current" user-id. + */ + foreach (cell, plist) + { + PlannedStmt *pstmt = lfirst(cell); + + if (IsA(pstmt, PlannedStmt) && OidIsValid(pstmt->planUserId)) + { + Assert(!OidIsValid(planUserId) || planUserId == pstmt->planUserId); + + planUserId = pstmt->planUserId; + } + } + + /* * Normally we make a dedicated memory context for the CachedPlan and its * subsidiary data. (It's probably not going to be large, but just in * case, use the default maxsize parameter. It's transient for the *************** BuildCachedPlan(CachedPlanSource *planso *** 956,961 **** --- 987,993 ---- plan->is_oneshot = plansource->is_oneshot; plan->is_saved = false; plan->is_valid = true; + plan->planUserId = planUserId; /* assign generation number to new plan */ plan->generation = ++(plansource->generation); diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c new file mode 100644 index d0acca8..2137e98 *** 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 *** 932,937 **** --- 933,943 ---- 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 *** 1840,1845 **** --- 1846,1853 ---- 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) *** 3166,3172 **** relation->rd_rel->relhastriggers = false; restart = true; } ! /* Release hold on the relation */ RelationDecrementReferenceCount(relation); --- 3174,3186 ---- 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) *** 4443,4448 **** --- 4457,4463 ---- 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 247ad92..9603ef4 *** 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 63a8009..25ff339 *** a/src/bin/pg_dump/pg_backup_archiver.c --- b/src/bin/pg_dump/pg_backup_archiver.c *************** _printTocEntry(ArchiveHandle *AH, TocEnt *** 3133,3138 **** --- 3133,3139 ---- 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 224e8cb..7d60cf1 *** a/src/bin/pg_dump/pg_dump.c --- b/src/bin/pg_dump/pg_dump.c *************** static char *myFormatType(const char *ty *** 250,255 **** --- 250,256 ---- 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) *** 2714,2719 **** --- 2715,2848 ---- 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) *** 4242,4247 **** --- 4371,4377 ---- 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) *** 4293,4302 **** "(%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, " --- 4423,4429 ---- "(%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) *** 4332,4337 **** --- 4459,4465 ---- "(%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) *** 4371,4376 **** --- 4499,4505 ---- "(%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) *** 4408,4413 **** --- 4537,4543 ---- "(%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) *** 4444,4449 **** --- 4574,4580 ---- "(%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) *** 4480,4485 **** --- 4611,4617 ---- "(%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) *** 4516,4521 **** --- 4648,4654 ---- "(%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) *** 4552,4557 **** --- 4685,4691 ---- "(%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) *** 4584,4589 **** --- 4718,4724 ---- "(%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) *** 4611,4616 **** --- 4746,4752 ---- "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) *** 4648,4653 **** --- 4784,4790 ---- "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) *** 4695,4700 **** --- 4832,4838 ---- 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) *** 4744,4749 **** --- 4882,4888 ---- 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 *** 7861,7866 **** --- 8000,8008 ---- 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 * *** 15118,15123 **** --- 15260,15266 ---- 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 915e82c..72c0433 *** 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 141e713..5c5777c *** a/src/bin/pg_dump/pg_dump_sort.c --- b/src/bin/pg_dump/pg_dump_sort.c *************** describeDumpableObject(DumpableObject *o *** 1342,1347 **** --- 1342,1352 ---- "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 96322ca..89e1c09 *** a/src/bin/psql/describe.c --- b/src/bin/psql/describe.c *************** listTables(const char *tabtypes, const c *** 2752,2757 **** --- 2752,2761 ---- appendPQExpBuffer(&buf, ",\n pg_catalog.obj_description(c.oid, 'pg_class') as \"%s\"", gettext_noop("Description")); + if (pset.sversion >= 90300) + 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 *** 2761,2766 **** --- 2765,2773 ---- 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 >= 90300) + 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 3aefbb5..c96e8d4 *** 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 4860e98..04dbeca *** 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 a1fee11..d937924 *** 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 29 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 ...798d556 *** 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_rowlevelsec definition. cpp turns this into + * typedef struct FormData_pg_rowlevelsec + * ---------------- + */ + #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_rowlevelsec corresponds to a pointer to a row with + * the format of pg_rowlevelsec relation. + * ---------------- + */ + typedef FormData_pg_rowsecurity *Form_pg_rowsecurity; + + /* ---------------- + * compiler constants for pg_rowlevelsec + * ---------------- + */ + #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 98ca553..8d76387 *** 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/execnodes.h b/src/include/nodes/execnodes.h new file mode 100644 index 5a40347..4133b8c *** a/src/include/nodes/execnodes.h --- b/src/include/nodes/execnodes.h *************** typedef struct JunkFilter *** 308,313 **** --- 308,315 ---- * ConstraintExprs array of constraint-checking expr states * junkFilter for removing junk attributes from tuples * projectReturning for computing a RETURNING list + * rowSecurity for row-security checks + * rowSecParams param-list if row-security has SubLink * ---------------- */ typedef struct ResultRelInfo *************** typedef struct ResultRelInfo *** 329,334 **** --- 331,338 ---- List **ri_ConstraintExprs; JunkFilter *ri_junkFilter; ProjectionInfo *ri_projectReturning; + Node *ri_rowSecurity; + List *ri_rowSecParams; } ResultRelInfo; /* ---------------- diff --git a/src/include/nodes/nodeFuncs.h b/src/include/nodes/nodeFuncs.h new file mode 100644 index fe7cfd3..23c3553 *** a/src/include/nodes/nodeFuncs.h --- b/src/include/nodes/nodeFuncs.h *************** *** 24,29 **** --- 24,30 ---- #define QTW_IGNORE_RANGE_TABLE 0x08 /* skip rangetable entirely */ #define QTW_EXAMINE_RTES 0x10 /* examine RTEs */ #define QTW_DONT_COPY_QUERY 0x20 /* do not copy top Query */ + #define QTW_IGNORE_RETURNING 0x40 /* skip returning clause */ extern Oid exprType(const Node *expr); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h new file mode 100644 index 6a5555f..8163708 *** a/src/include/nodes/parsenodes.h --- b/src/include/nodes/parsenodes.h *************** typedef enum QuerySource *** 31,37 **** QSRC_PARSER, /* added by parse analysis (now unused) */ QSRC_INSTEAD_RULE, /* added by unconditional INSTEAD rule */ QSRC_QUAL_INSTEAD_RULE, /* added by conditional INSTEAD rule */ ! QSRC_NON_INSTEAD_RULE /* added by non-INSTEAD rule */ } QuerySource; /* Sort ordering options for ORDER BY and CREATE INDEX */ --- 31,38 ---- QSRC_PARSER, /* added by parse analysis (now unused) */ QSRC_INSTEAD_RULE, /* added by unconditional INSTEAD rule */ QSRC_QUAL_INSTEAD_RULE, /* added by conditional INSTEAD rule */ ! QSRC_NON_INSTEAD_RULE, /* added by non-INSTEAD rule */ ! QSRC_ROW_SECURITY, /* added by row-security */ } QuerySource; /* Sort ordering options for ORDER BY and CREATE INDEX */ *************** typedef struct Query *** 112,118 **** int resultRelation; /* rtable index of target relation for * INSERT/UPDATE/DELETE; 0 for SELECT */ ! bool hasAggs; /* has aggregates in tlist or havingQual */ bool hasWindowFuncs; /* has window functions in tlist */ bool hasSubLinks; /* has subquery SubLink */ --- 113,121 ---- int resultRelation; /* rtable index of target relation for * INSERT/UPDATE/DELETE; 0 for SELECT */ ! int sourceRelation; /* rtable index of source relation for ! * UPDATE/DELETE, if not identical with ! * resultRelation; 0 for elsewhere */ bool hasAggs; /* has aggregates in tlist or havingQual */ bool hasWindowFuncs; /* has window functions in tlist */ bool hasSubLinks; /* has subquery SubLink */ *************** typedef struct RangeTblEntry *** 739,744 **** --- 742,752 ---- */ Query *subquery; /* the sub-query */ bool security_barrier; /* is from security_barrier view? */ + Oid rowsec_relid; /* OID of the original relation, if this + * sub-query originated from row-security + * policy on the relation. Elsewhere, it + * should be InvalidOid. + */ /* * Fields valid for a join RTE (else NULL/zero): *************** typedef enum AlterTableType *** 1314,1319 **** --- 1322,1329 ---- 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 101e22c..e675e87 *** 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 6d7b594..ee454fa *** a/src/include/nodes/relation.h --- b/src/include/nodes/relation.h *************** typedef struct PlannerGlobal *** 98,103 **** --- 98,105 ---- Index lastRowMarkId; /* highest PlanRowMark ID assigned */ bool transientPlan; /* redo plan when TransactionXmin changes? */ + + Oid planUserId; /* User-Id to be assumed on this plan */ } PlannerGlobal; /* macro for fetching the Plan associated with a SubPlan node */ *************** typedef struct AppendRelInfo *** 1435,1440 **** --- 1437,1446 ---- */ Index parent_relid; /* RT index of append parent rel */ Index child_relid; /* RT index of append child rel */ + Index child_result; /* RT index of append child rel's source, + * if source of result relation is not + * identical. Elsewhere, 0. + */ /* * For an inheritance appendrel, the parent and child are both regular diff --git a/src/include/optimizer/rowsecurity.h b/src/include/optimizer/rowsecurity.h new file mode 100644 index ...ff4dd9a *** a/src/include/optimizer/rowsecurity.h --- b/src/include/optimizer/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 bool copy_row_security_policy(CopyStmt *stmt, + Relation relation, List *attnums); + extern void apply_row_security_policy(PlannerInfo *root); + + #endif /* ROWSECURITY_H */ diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h new file mode 100644 index bea3b07..3910853 *** 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/rewriteHandler.h b/src/include/rewrite/rewriteHandler.h new file mode 100644 index c959590..ef10f56 *** a/src/include/rewrite/rewriteHandler.h --- b/src/include/rewrite/rewriteHandler.h *************** *** 18,23 **** --- 18,24 ---- #include "nodes/parsenodes.h" extern List *QueryRewrite(Query *parsetree); + extern void QueryRewriteExpr(Node *node, List *activeRIRs); extern void AcquireRewriteLocks(Query *parsetree, bool forUpdatePushedDown); extern Node *build_column_default(Relation rel, int attrno); diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h new file mode 100644 index 72f8491..35819fa *** a/src/include/utils/plancache.h --- b/src/include/utils/plancache.h *************** typedef struct CachedPlan *** 128,133 **** --- 128,135 ---- bool is_oneshot; /* is it a "oneshot" plan? */ bool is_saved; /* is CachedPlan in a long-lived context? */ bool is_valid; /* is the stmt_list currently valid? */ + Oid planUserId; /* is user-id that is assumed on this cached + plan, or InvalidOid if portable for anybody */ TransactionId saved_xmin; /* if valid, replan when TransactionXmin * changes from this value */ int generation; /* parent's generation number for this plan */ diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h new file mode 100644 index 21d5871..9a324ab *** 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 *** 109,114 **** --- 110,116 ---- 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 ...04591df *** a/src/test/regress/expected/rowsecurity.out --- b/src/test/regress/expected/rowsecurity.out *************** *** 0 **** --- 1,950 ---- + -- + -- 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); + NOTICE: f_leak => my first novel + NOTICE: f_leak => my first manga + NOTICE: f_leak => great science fiction + NOTICE: f_leak => great manga + did | cid | dlevel | dauthor | dtitle + -----+-----+--------+-------------------+----------------------- + 1 | 11 | 1 | rls_regress_user1 | my first novel + 4 | 44 | 1 | rls_regress_user1 | my first manga + 6 | 22 | 1 | rls_regress_user2 | great science fiction + 8 | 44 | 1 | rls_regress_user2 | great manga + (4 rows) + + SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle); + NOTICE: f_leak => my first novel + NOTICE: f_leak => my first manga + NOTICE: f_leak => great science fiction + NOTICE: f_leak => great manga + cid | did | dlevel | dauthor | dtitle | cname + -----+-----+--------+-------------------+-----------------------+----------------- + 11 | 1 | 1 | rls_regress_user1 | my first novel | novel + 22 | 6 | 1 | rls_regress_user2 | great science fiction | science fiction + 44 | 8 | 1 | rls_regress_user2 | great manga | manga + 44 | 4 | 1 | rls_regress_user1 | my first manga | manga + (4 rows) + + -- viewpoint from rls_regress_user2 + SET SESSION AUTHORIZATION rls_regress_user2; + SELECT * FROM document WHERE f_leak(dtitle); + NOTICE: f_leak => my first novel + NOTICE: f_leak => my second novel + NOTICE: f_leak => my science fiction + NOTICE: f_leak => my first manga + NOTICE: f_leak => my second manga + NOTICE: f_leak => great science fiction + NOTICE: f_leak => great technology book + NOTICE: f_leak => great manga + did | cid | dlevel | dauthor | dtitle + -----+-----+--------+-------------------+----------------------- + 1 | 11 | 1 | rls_regress_user1 | my first novel + 2 | 11 | 2 | rls_regress_user1 | my second novel + 3 | 22 | 2 | rls_regress_user1 | my science fiction + 4 | 44 | 1 | rls_regress_user1 | my first manga + 5 | 44 | 2 | rls_regress_user1 | my second manga + 6 | 22 | 1 | rls_regress_user2 | great science fiction + 7 | 33 | 2 | rls_regress_user2 | great technology book + 8 | 44 | 1 | rls_regress_user2 | great manga + (8 rows) + + SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle); + NOTICE: f_leak => my first novel + NOTICE: f_leak => my second novel + NOTICE: f_leak => my science fiction + NOTICE: f_leak => my first manga + NOTICE: f_leak => my second manga + NOTICE: f_leak => great science fiction + NOTICE: f_leak => great technology book + NOTICE: f_leak => great manga + cid | did | dlevel | dauthor | dtitle | cname + -----+-----+--------+-------------------+-----------------------+----------------- + 11 | 2 | 2 | rls_regress_user1 | my second novel | novel + 11 | 1 | 1 | rls_regress_user1 | my first novel | novel + 22 | 6 | 1 | rls_regress_user2 | great science fiction | science fiction + 22 | 3 | 2 | rls_regress_user1 | my science fiction | science fiction + 33 | 7 | 2 | rls_regress_user2 | great technology book | technology + 44 | 8 | 1 | rls_regress_user2 | great manga | manga + 44 | 5 | 2 | rls_regress_user1 | my second manga | manga + 44 | 4 | 1 | rls_regress_user1 | my first manga | manga + (8 rows) + + EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle); + QUERY PLAN + ---------------------------------------------------------- + Subquery Scan on document + Filter: f_leak(document.dtitle) + -> Seq Scan on document document_1 + Filter: (dlevel <= $0) + InitPlan 1 (returns $0) + -> Index Scan using uaccount_pkey on uaccount + Index Cond: (pguser = "current_user"()) + (7 rows) + + EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle); + QUERY PLAN + ---------------------------------------------------------------------- + Hash Join + Hash Cond: (category.cid = document.cid) + -> Seq Scan on category + -> Hash + -> Subquery Scan on document + Filter: f_leak(document.dtitle) + -> Seq Scan on document document_1 + Filter: (dlevel <= $0) + InitPlan 1 (returns $0) + -> Index Scan using uaccount_pkey on uaccount + Index Cond: (pguser = "current_user"()) + (11 rows) + + -- only owner can change row-level security + ALTER TABLE document SET ROW 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); + NOTICE: f_leak => my first novel + NOTICE: f_leak => my second novel + NOTICE: f_leak => my science fiction + NOTICE: f_leak => my first manga + NOTICE: f_leak => my second manga + did | cid | dlevel | dauthor | dtitle + -----+-----+--------+-------------------+-------------------- + 1 | 11 | 1 | rls_regress_user1 | my first novel + 2 | 11 | 2 | rls_regress_user1 | my second novel + 3 | 22 | 2 | rls_regress_user1 | my science fiction + 4 | 44 | 1 | rls_regress_user1 | my first manga + 5 | 44 | 2 | rls_regress_user1 | my second manga + (5 rows) + + SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle); + NOTICE: f_leak => my first novel + NOTICE: f_leak => my second novel + NOTICE: f_leak => my science fiction + NOTICE: f_leak => my first manga + NOTICE: f_leak => my second manga + cid | did | dlevel | dauthor | dtitle | cname + -----+-----+--------+-------------------+--------------------+----------------- + 11 | 1 | 1 | rls_regress_user1 | my first novel | novel + 11 | 2 | 2 | rls_regress_user1 | my second novel | novel + 22 | 3 | 2 | rls_regress_user1 | my science fiction | science fiction + 44 | 4 | 1 | rls_regress_user1 | my first manga | manga + 44 | 5 | 2 | rls_regress_user1 | my second manga | manga + (5 rows) + + -- viewpoint from rls_regress_user2 again + SET SESSION AUTHORIZATION rls_regress_user2; + SELECT * FROM document WHERE f_leak(dtitle); + NOTICE: f_leak => great science fiction + NOTICE: f_leak => great technology book + NOTICE: f_leak => great manga + did | cid | dlevel | dauthor | dtitle + -----+-----+--------+-------------------+----------------------- + 6 | 22 | 1 | rls_regress_user2 | great science fiction + 7 | 33 | 2 | rls_regress_user2 | great technology book + 8 | 44 | 1 | rls_regress_user2 | great manga + (3 rows) + + SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle); + NOTICE: f_leak => great science fiction + NOTICE: f_leak => great technology book + NOTICE: f_leak => great manga + cid | did | dlevel | dauthor | dtitle | cname + -----+-----+--------+-------------------+-----------------------+----------------- + 22 | 6 | 1 | rls_regress_user2 | great science fiction | science fiction + 33 | 7 | 2 | rls_regress_user2 | great technology book | technology + 44 | 8 | 1 | rls_regress_user2 | great manga | manga + (3 rows) + + EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle); + QUERY PLAN + ---------------------------------------------- + Subquery Scan on document + Filter: f_leak(document.dtitle) + -> Seq Scan on document document_1 + Filter: (dauthor = "current_user"()) + (4 rows) + + EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle); + QUERY PLAN + ---------------------------------------------------- + Nested Loop + -> Subquery Scan on document + Filter: f_leak(document.dtitle) + -> Seq Scan on document document_1 + Filter: (dauthor = "current_user"()) + -> Index Scan using category_pkey on category + Index Cond: (cid = document.cid) + (7 rows) + + -- interaction of FK/PK constraints + SET SESSION AUTHORIZATION rls_regress_user0; + ALTER TABLE category SET ROW 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; -- failed + ERROR: update or delete on table "category" violates foreign key constraint "document_cid_fkey" on table "document" + DETAIL: Key (cid)=(33) is still referenced from table "document". + -- cannot insert FK referencing invisible PK + SET SESSION AUTHORIZATION rls_regress_user2; + SELECT * FROM document d full outer join category c on d.cid = c.cid; + did | cid | dlevel | dauthor | dtitle | cid | cname + -----+-----+--------+-------------------+-----------------------+-----+----------------- + 6 | 22 | 1 | rls_regress_user2 | great science fiction | 22 | science fiction + 8 | 44 | 1 | rls_regress_user2 | great manga | 44 | manga + 7 | 33 | 2 | rls_regress_user2 | great technology book | | + (3 rows) + + INSERT INTO document VALUES (10, 33, 1, current_user, 'hoge'); -- failed + ERROR: insert or update on table "document" violates foreign key constraint "document_cid_fkey" + DETAIL: Key (cid)=(33) is not present in table "category". + -- database superuser can bypass RLS policy + RESET SESSION AUTHORIZATION; + SELECT * FROM document; + did | cid | dlevel | dauthor | dtitle + -----+-----+--------+-------------------+----------------------- + 1 | 11 | 1 | rls_regress_user1 | my first novel + 2 | 11 | 2 | rls_regress_user1 | my second novel + 3 | 22 | 2 | rls_regress_user1 | my science fiction + 4 | 44 | 1 | rls_regress_user1 | my first manga + 5 | 44 | 2 | rls_regress_user1 | my second manga + 6 | 22 | 1 | rls_regress_user2 | great science fiction + 7 | 33 | 2 | rls_regress_user2 | great technology book + 8 | 44 | 1 | rls_regress_user2 | great manga + (8 rows) + + SELECT * FROM category; + cid | cname + -----+----------------- + 11 | novel + 22 | science fiction + 33 | technology + 44 | manga + (4 rows) + + -- + -- Table inheritance and RLS policy + -- + SET SESSION AUTHORIZATION rls_regress_user0; + CREATE TABLE t1 (a int, junk1 text, b text) WITH OIDS; + ALTER TABLE t1 DROP COLUMN junk1; -- just a disturbing factor + GRANT ALL ON t1 TO public; + COPY t1 FROM stdin WITH (oids); + CREATE TABLE t2 (c float) INHERITS (t1); + COPY t2 FROM stdin WITH (oids); + CREATE TABLE t3 (c text, b text, a int) WITH OIDS; + ALTER TABLE t3 INHERIT t1; + COPY t3(a,b,c) FROM stdin WITH (oids); + ALTER TABLE t1 SET ROW 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 + 1 | abc + 3 | cde + 1 | xxx + 2 | yyy + 3 | zzz + (7 rows) + + EXPLAIN (costs off) SELECT * FROM t1; + QUERY PLAN + ------------------------------------- + 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 + (8 rows) + + SELECT * FROM t1 WHERE f_leak(b); + NOTICE: f_leak => bbb + NOTICE: f_leak => ddd + NOTICE: f_leak => abc + NOTICE: f_leak => cde + NOTICE: f_leak => xxx + NOTICE: f_leak => yyy + NOTICE: f_leak => zzz + a | b + ---+----- + 2 | bbb + 4 | ddd + 1 | abc + 3 | cde + 1 | xxx + 2 | yyy + 3 | zzz + (7 rows) + + EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b); + QUERY PLAN + ------------------------------------- + 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) + (11 rows) + + -- reference to system column + SELECT oid, * FROM t1; + oid | a | b + -----+---+----- + 102 | 2 | bbb + 104 | 4 | ddd + 201 | 1 | abc + 203 | 3 | cde + 301 | 1 | xxx + 302 | 2 | yyy + 303 | 3 | zzz + (7 rows) + + EXPLAIN (costs off) SELECT * FROM t1; + QUERY PLAN + ------------------------------------- + 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 + (8 rows) + + -- reference to whole-row reference + SELECT *,t1 FROM t1; + a | b | t1 + ---+-----+--------- + 2 | bbb | (2,bbb) + 4 | ddd | (4,ddd) + 1 | abc | (1,abc) + 3 | cde | (3,cde) + 1 | xxx | (1,xxx) + 2 | yyy | (2,yyy) + 3 | zzz | (3,zzz) + (7 rows) + + EXPLAIN (costs off) SELECT *,t1 FROM t1; + QUERY PLAN + ------------------------------------- + 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 + (8 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) + + -- + -- COPY TO statement + -- + COPY t1 TO stdout; + 2 bbb + 4 ddd + COPY t1 TO stdout WITH OIDS; + 102 2 bbb + 104 4 ddd + COPY t2(c,b) TO stdout WITH OIDS; + 201 1.1 abc + 203 3.3 cde + COPY (SELECT * FROM t1) TO stdout; + 2 bbb + 4 ddd + 1 abc + 3 cde + 1 xxx + 2 yyy + 3 zzz + COPY document TO stdout WITH OIDS; -- failed (no oid column) + ERROR: table "document" does not have OIDs + -- + -- recursive RLS and VIEWs in policy + -- + CREATE TABLE s1 (a int, b text); + INSERT INTO s1 (SELECT x, md5(x::text) FROM generate_series(-10,10) x); + CREATE TABLE s2 (x int, y text); + INSERT INTO s2 (SELECT x, md5(x::text) FROM generate_series(-6,6) x); + CREATE VIEW v2 AS SELECT * FROM s2 WHERE y like '%af%'; + ALTER TABLE s1 SET ROW 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) + -> Subquery Scan on s2 (cost=0.00..28.52 rows=1 width=4) + Filter: (s2.y ~~ '%2f%'::text) + -> Seq Scan on s2 s2_1 (cost=0.00..28.45 rows=6 width=36) + Filter: ((x % 2) = 0) + (11 rows) + + ALTER TABLE s1 SET ROW 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 + -> Subquery Scan on s2 + Filter: (s2.y ~~ '%af%'::text) + -> Seq Scan on s2 s2_1 + Filter: ((x % 2) = 0) + (11 rows) + + SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%'; + xx | x | y + ----+----+---------------------------------- + -6 | -6 | 596a3d04481816330f07e4f97510c28f + -4 | -4 | 0267aaf632e87a63288a08331f22c7c3 + 2 | 2 | c81e728d9d4c2f636f067f89cc14862c + (3 rows) + + EXPLAIN (COSTS OFF) SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%'; + QUERY PLAN + -------------------------------------------------------------------- + Subquery Scan on s2 + Filter: (s2.y ~~ '%28%'::text) + -> Seq Scan on s2 s2_1 + Filter: ((x % 2) = 0) + SubPlan 1 + -> Limit + -> Subquery Scan on s1 + -> Nested Loop Semi Join + Join Filter: (s1_1.a = s2_2.x) + -> Seq Scan on s1 s1_1 + -> Materialize + -> Subquery Scan on s2_2 + Filter: (s2_2.y ~~ '%af%'::text) + -> Seq Scan on s2 s2_3 + Filter: ((x % 2) = 0) + (15 rows) + + ALTER TABLE s2 SET ROW 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 a62a3e3..a41344b *** 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 5758b07..f7963fc *** 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 78348f5..51c4c12 *** a/src/test/regress/serial_schedule --- b/src/test/regress/serial_schedule *************** test: delete *** 94,99 **** --- 94,100 ---- 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 ...55d4aad *** a/src/test/regress/sql/rowsecurity.sql --- b/src/test/regress/sql/rowsecurity.sql *************** *** 0 **** --- 1,298 ---- + -- + -- 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); + SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle); + + -- viewpoint from rls_regress_user2 + SET SESSION AUTHORIZATION rls_regress_user2; + SELECT * FROM document WHERE f_leak(dtitle); + SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle); + + EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle); + EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle); + + -- only owner can change row-level security + ALTER TABLE document SET ROW 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); + SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle); + + -- viewpoint from rls_regress_user2 again + SET SESSION AUTHORIZATION rls_regress_user2; + SELECT * FROM document WHERE f_leak(dtitle); + SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle); + + EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle); + EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle); + + -- interaction of FK/PK constraints + SET SESSION AUTHORIZATION rls_regress_user0; + ALTER TABLE category SET ROW 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; -- failed + + -- cannot insert FK referencing invisible PK + SET SESSION AUTHORIZATION rls_regress_user2; + SELECT * FROM document d full outer join category c on d.cid = c.cid; + INSERT INTO document VALUES (10, 33, 1, current_user, 'hoge'); -- failed + + -- database superuser can bypass RLS policy + RESET SESSION AUTHORIZATION; + SELECT * FROM document; + SELECT * FROM category; + + -- + -- Table inheritance and RLS policy + -- + SET SESSION AUTHORIZATION rls_regress_user0; + + CREATE TABLE t1 (a int, junk1 text, b text) WITH OIDS; + ALTER TABLE t1 DROP COLUMN junk1; -- just a disturbing factor + GRANT ALL ON t1 TO public; + + COPY t1 FROM stdin WITH (oids); + 101 1 aaa + 102 2 bbb + 103 3 ccc + 104 4 ddd + \. + + CREATE TABLE t2 (c float) INHERITS (t1); + COPY t2 FROM stdin WITH (oids); + 201 1 abc 1.1 + 202 2 bcd 2.2 + 203 3 cde 3.3 + 204 4 def 4.4 + \. + + CREATE TABLE t3 (c text, b text, a int) WITH OIDS; + ALTER TABLE t3 INHERIT t1; + COPY t3(a,b,c) FROM stdin WITH (oids); + 301 1 xxx X + 302 2 yyy Y + 303 3 zzz Z + \. + + ALTER TABLE t1 SET ROW 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; + + -- + -- COPY TO statement + -- + COPY t1 TO stdout; + COPY t1 TO stdout WITH OIDS; + COPY t2(c,b) TO stdout WITH OIDS; + COPY (SELECT * FROM t1) TO stdout; + COPY document TO stdout WITH OIDS; -- failed (no oid column) + + -- + -- recursive RLS and VIEWs in policy + -- + CREATE TABLE s1 (a int, b text); + INSERT INTO s1 (SELECT x, md5(x::text) FROM generate_series(-10,10) x); + + CREATE TABLE s2 (x int, y text); + INSERT INTO s2 (SELECT x, md5(x::text) FROM generate_series(-6,6) x); + CREATE VIEW v2 AS SELECT * FROM s2 WHERE y like '%af%'; + + ALTER TABLE s1 SET ROW 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;