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