doc/src/sgml/catalogs.sgml | 59 ++ doc/src/sgml/ref/alter_table.sgml | 39 ++ doc/src/sgml/user-manag.sgml | 145 ++++ src/backend/catalog/Makefile | 4 +- src/backend/catalog/dependency.c | 20 + src/backend/catalog/heap.c | 1 + src/backend/catalog/pg_rowsecurity.c | 288 ++++++++ src/backend/commands/copy.c | 94 ++- src/backend/commands/explain.c | 8 +- src/backend/commands/tablecmds.c | 27 + src/backend/executor/execMain.c | 161 ++++- src/backend/executor/nodeModifyTable.c | 6 +- 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/rowsecurity.c | 845 +++++++++++++++++++++++ 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/bin/psql/describe.c | 7 + src/include/catalog/dependency.h | 1 + src/include/catalog/indexing.h | 3 + src/include/catalog/pg_class.h | 20 +- src/include/catalog/pg_rowsecurity.h | 58 ++ src/include/commands/copy.h | 2 +- src/include/miscadmin.h | 1 + src/include/nodes/execnodes.h | 4 + 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/rowsecurity.h | 29 + 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/rowsecurity.out | 1007 ++++++++++++++++++++++++++++ src/test/regress/expected/sanity_check.out | 3 +- src/test/regress/parallel_schedule | 2 +- src/test/regress/serial_schedule | 1 + src/test/regress/sql/rowsecurity.sql | 331 +++++++++ 49 files changed, 3480 insertions(+), 60 deletions(-) diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index 68092ec..f61263e 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 @@ + relhasrowsecurity + bool + + + True if table has row-security policy; see + pg_rowsecurity catalog + + + + relhassubclass bool @@ -4917,6 +4932,50 @@ + + <structname>pg_rowlevelsec</structname> + + + pg_rowsecurity + + + The catalog pg_rowsecurity stores expression + tree of row-security policy to be performed on a particular relation. + + + <structname>pg_rowsecurity</structname> Columns + + + + Name + Type + References + Description + + + + + rsecrelid + oid + pg_class.oid + The table this row-security is for + + + rsecqual + text + + An expression tree to be performed as rowl-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 5437626..ed61dea 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 SECURITY (condition) + RESET ROW SECURITY and table_constraint_using_index is: @@ -565,6 +567,29 @@ ALTER TABLE [ IF EXISTS ] name + SET ROW 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 SECURITY + + + This form reset row-level security policy of the table, if exists. + + + + + RENAME @@ -806,6 +831,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..719393a 100644 --- a/doc/src/sgml/user-manag.sgml +++ b/doc/src/sgml/user-manag.sgml @@ -439,4 +439,149 @@ DROP ROLE name; + + Row-security + + PostgreSQL v9.3 or later provides + row-security feature, like several commercial database + management system. It allows table owner to assign a particular + condition that performs as a security policy of the table; only + rows that satisfies the condition should be visible, except for + a case when superuser runs queries. + + + Row-security policy can be set using SET ROW SECURITY + command of statement, as an expression + form that returns a value of type boolean. This expression can contain + references to columns of the relation, so it enables to construct + arbitrary rule to make access control decision based on contents of + each rows. + + + For example, the following customer table has + uname field to store user name, and it assume + we don't want to expose any properties of other customers. + The following command set current_user = uname + as row-security policy on the customer table. + +postgres=> ALTER TABLE customer SET ROW SECURITY (current_user = uname); +ALTER TABLE + + command shows how row-security policy + works on the supplied query. + +postgres=> EXPLAIN(costs off) SELECT * FROM customer WHERE f_leak(upasswd); + QUERY PLAN +-------------------------------------------- + Subquery Scan on customer + Filter: f_leak(customer.upasswd) + -> Seq Scan on customer customer_1 + Filter: ("current_user"() = uname) +(4 rows) + + This query execution plan means the preconfigured row-security policy is + implicitly added, and scan plan on the target relation being wrapped up + with a sub-query. + It ensures user given qualifiers, including functions with side effects, + are never executed earlier than the row-security policy regardless of + its cost, except for the cases when these were fully leakproof. + This design helps to tackle the scenario described in + ; that introduces the order to evaluate + qualifiers is significant to keep confidentiality of invisible rows. + + + + On the other hand, this design allows superusers to bypass checks with + row-security. + It ensures pg_dump can obtain a complete set + of database backup, and avoid to execute Trojan horse trap, being injected + as a row-security policy of user-defined table, with privileges of + superuser. + + + + In case of queries on inherited tables, row-security policy of the parent + relation is not applied to child relations. Scope of the row-security + policy is limited to the relation on which it is set. + +postgres=> EXPLAIN(costs off) SELECT * FROM t1 WHERE f_leak(y); + QUERY PLAN +------------------------------------------- + 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-security policy is set on only t1, + and t3, not t2. + + The row-security policy of t1, x + must be even-number, is appended only t1, + neither t2 nor t3. + On the contrary, t3 has different row-security policy; + x must be odd-number. + + + + Row-security feature also works to queries for writer-operations; such as + , or + commands. + It ensures all the modified rows satisfies configured row-security policy. + The below query tries to update e-mail address of the + customer table, and configured row-security makes sure all the + modified rows's uname field has to match with + current_user. + + +postgres=> EXPLAIN (costs off) UPDATE customer SET email = 'alice@example.com'; + QUERY PLAN +-------------------------------------------------- + Update on customer + -> Subquery Scan on customer + -> Seq Scan on customer customer_1 + Filter: ("current_user"() = uname) +(4 rows) + + + Modification of a certain table is consist of two different stuffs; one + is fetch rows to be modified from the result relation (except for + INSERT command), second is insertion of rows to the + result relation. Usually, before-row trigger or check constraints are + run in the second phase. In addition, row-security policy is also checked + on this stage, to prevent to insert or update rows with unprivileged + values. + + + + Unlike other commercial database systems, we don't have any plan to allow + individual row-security policy for each command type. Even if we want to + perform with difference policy between and + , RETURNING clause can leak the rows + to be invisible using command. + + + + Even though it is not a specific matter in row-security, please be careful + if you plan to use current_user in row-level security policy. + and allows to + switch current user identifier during execution of the query. + Thus, it can fetch the first 100 rows with privilege of alice, + then remaining rows with privilege of bob. If and when query + execution plan contains some kind of materialization and row-security + policy contains current_user, the fetched tuples in + bob's screen might be evaluated according to the privilege of + alice. + + diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile index df6da1f..c9de8a8 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_rowsecurity.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_rowsecurity.h \ pg_default_acl.h pg_seclabel.h pg_shseclabel.h pg_collation.h pg_range.h \ toasting.h indexing.h \ ) diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index 192b421..2d2328e 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_rowsecurity.h" #include "catalog/pg_tablespace.h" #include "catalog/pg_trigger.h" #include "catalog/pg_ts_config.h" @@ -1238,6 +1239,10 @@ doDeletion(const ObjectAddress *object, int flags) RemoveEventTriggerById(object->objectId); break; + case OCLASS_ROWSECURITY: + RemoveRowSecurityById(object->objectId); + break; + default: elog(ERROR, "unrecognized object class: %u", object->classId); @@ -2297,6 +2302,9 @@ getObjectClass(const ObjectAddress *object) case EventTriggerRelationId: return OCLASS_EVENT_TRIGGER; + + case RowSecurityRelationId: + return OCLASS_ROWSECURITY; } /* shouldn't get here */ @@ -2951,6 +2959,18 @@ getObjectDescription(const ObjectAddress *object) break; } + case OCLASS_ROWSECURITY: + { + char *relname; + + relname = get_rel_name(object->objectId); + if (!relname) + elog(ERROR, "cache lookup failed for relation %u", + object->objectId); + appendStringInfo(&buffer, _("row-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 d93d273..25ecaf3 100644 --- a/src/backend/catalog/heap.c +++ b/src/backend/catalog/heap.c @@ -777,6 +777,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_relhasrowsecurity - 1] = BoolGetDatum(rd_rel->relhasrowsecurity); 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_rowsecurity.c b/src/backend/catalog/pg_rowsecurity.c new file mode 100644 index 0000000..9706ba4 --- /dev/null +++ b/src/backend/catalog/pg_rowsecurity.c @@ -0,0 +1,288 @@ +/* ------------------------------------------------------------------------- + * + * pg_rowsecurity.c + * routines to support manipulation of the pg_rowsecurity catalog + * + * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * ------------------------------------------------------------------------- + */ +#include "postgres.h" +#include "access/genam.h" +#include "access/heapam.h" +#include "access/htup_details.h" +#include "catalog/dependency.h" +#include "catalog/indexing.h" +#include "catalog/pg_class.h" +#include "catalog/pg_rowsecurity.h" +#include "catalog/pg_type.h" +#include "nodes/nodeFuncs.h" +#include "optimizer/clauses.h" +#include "parser/parse_clause.h" +#include "parser/parse_node.h" +#include "parser/parse_relation.h" +#include "utils/builtins.h" +#include "utils/fmgroids.h" +#include "utils/rel.h" +#include "utils/syscache.h" +#include "utils/tqual.h" + +/* + * Load row-security policy from the catalog, and keep it on + * the relation cache. + */ +void +RelationBuildRowSecurity(Relation relation) +{ + Relation catalog; + ScanKeyData skey; + SysScanDesc sscan; + HeapTuple tuple; + + catalog = heap_open(RowSecurityRelationId, AccessShareLock); + + ScanKeyInit(&skey, + Anum_pg_rowsecurity_rsecrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationGetRelid(relation))); + sscan = systable_beginscan(catalog, RowSecurityIndexId, true, + SnapshotNow, 1, &skey); + + tuple = systable_getnext(sscan); + if (HeapTupleIsValid(tuple)) + { + RowSecurityDesc *rsdesc; + MemoryContext rscxt; + MemoryContext oldcxt; + Datum datum; + bool isnull; + char *temp; + + /* + * Make the private memory context to store RowSecurityDesc that + * includes expression tree also. + */ + rscxt = AllocSetContextCreate(CacheMemoryContext, + RelationGetRelationName(relation), + ALLOCSET_SMALL_MINSIZE, + ALLOCSET_SMALL_INITSIZE, + ALLOCSET_SMALL_MAXSIZE); + PG_TRY(); + { + datum = heap_getattr(tuple, Anum_pg_rowsecurity_rsecqual, + RelationGetDescr(catalog), &isnull); + Assert(!isnull); + temp = TextDatumGetCString(datum); + + oldcxt = MemoryContextSwitchTo(rscxt); + + rsdesc = palloc0(sizeof(RowSecurityDesc)); + rsdesc->rscxt = rscxt; + rsdesc->rsqual = (Expr *) stringToNode(temp); + Assert(exprType((Node *)rsdesc->rsqual) == BOOLOID); + + rsdesc->rshassublinks + = contain_subplans((Node *)rsdesc->rsqual); + + MemoryContextSwitchTo(oldcxt); + + pfree(temp); + } + PG_CATCH(); + { + MemoryContextDelete(rscxt); + PG_RE_THROW(); + } + PG_END_TRY(); + + relation->rsdesc = rsdesc; + } + else + { + relation->rsdesc = NULL; + } + systable_endscan(sscan); + heap_close(catalog, AccessShareLock); +} + +/* + * Parse the supplied row-security policy, and insert/update a row + * of pg_rowsecurity catalog. + */ +static void +InsertOrUpdatePolicyRow(Relation relation, Node *clause) +{ + Oid relationId = RelationGetRelid(relation); + ParseState *pstate; + RangeTblEntry *rte; + Node *qual; + Relation catalog; + ScanKeyData skey; + SysScanDesc sscan; + HeapTuple oldtup; + HeapTuple newtup; + Datum values[Natts_pg_rowsecurity]; + bool isnull[Natts_pg_rowsecurity]; + bool replaces[Natts_pg_rowsecurity]; + ObjectAddress target; + ObjectAddress myself; + + /* Parse the supplied clause */ + pstate = make_parsestate(NULL); + + rte = addRangeTableEntryForRelation(pstate, relation, + NULL, false, false); + addRTEtoQuery(pstate, rte, false, true, true); + + qual = transformWhereClause(pstate, copyObject(clause), + EXPR_KIND_ROW_SECURITY, + "ROW SECURITY"); + /* zero-clear */ + memset(values, 0, sizeof(values)); + memset(replaces, 0, sizeof(replaces)); + memset(isnull, 0, sizeof(isnull)); + + /* Update or Insert an entry to pg_rowsecurity catalog */ + catalog = heap_open(RowSecurityRelationId, RowExclusiveLock); + + ScanKeyInit(&skey, + Anum_pg_rowsecurity_rsecrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationGetRelid(relation))); + sscan = systable_beginscan(catalog, RowSecurityIndexId, true, + SnapshotNow, 1, &skey); + oldtup = systable_getnext(sscan); + if (HeapTupleIsValid(oldtup)) + { + replaces[Anum_pg_rowsecurity_rsecqual - 1] = true; + values[Anum_pg_rowsecurity_rsecqual - 1] + = CStringGetTextDatum(nodeToString(qual)); + + newtup = heap_modify_tuple(oldtup, + RelationGetDescr(catalog), + values, isnull, replaces); + simple_heap_update(catalog, &newtup->t_self, newtup); + + deleteDependencyRecordsFor(RowSecurityRelationId, + relationId, false); + } + else + { + values[Anum_pg_rowsecurity_rsecrelid - 1] + = ObjectIdGetDatum(relationId); + values[Anum_pg_rowsecurity_rsecqual - 1] + = CStringGetTextDatum(nodeToString(qual)); + newtup = heap_form_tuple(RelationGetDescr(catalog), + values, isnull); + simple_heap_insert(catalog, newtup); + } + CatalogUpdateIndexes(catalog, newtup); + + heap_freetuple(newtup); + + /* records dependencies of row-security policy and relation/columns */ + target.classId = RelationRelationId; + target.objectId = relationId; + target.objectSubId = 0; + + myself.classId = RowSecurityRelationId; + myself.objectId = 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(catalog, RowExclusiveLock); +} + +/* + * Remove row-security policy row of pg_rowsecurity + */ +static void +DeletePolicyRow(Relation relation) +{ + if (relation->rsdesc) + { + ObjectAddress address; + + address.classId = RowSecurityRelationId; + address.objectId = RelationGetRelid(relation); + address.objectSubId = 0; + + performDeletion(&address, DROP_RESTRICT, 0); + } + else + { + /* Nothing to do here */ + elog(INFO, "relation %s has no row-security policy, skipped", + RelationGetRelationName(relation)); + } +} + +/* + * Guts of row-security policy deletion. + */ +void +RemoveRowSecurityById(Oid relationId) +{ + Relation catalog; + ScanKeyData skey; + SysScanDesc sscan; + HeapTuple tuple; + + catalog = heap_open(RowSecurityRelationId, RowExclusiveLock); + + ScanKeyInit(&skey, + Anum_pg_rowsecurity_rsecrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(relationId)); + sscan = systable_beginscan(catalog, RowSecurityIndexId, true, + SnapshotNow, 1, &skey); + while (HeapTupleIsValid(tuple = systable_getnext(sscan))) + { + simple_heap_delete(catalog, &tuple->t_self); + } + systable_endscan(sscan); + heap_close(catalog, RowExclusiveLock); +} + +/* + * ALTER TABLE SET ROW SECURITY (...) OR + * RESET ROW SECURITY + */ +void +ATExecSetRowSecurity(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->relhasrowsecurity = true; + } + else + { + DeletePolicyRow(relation); + class_form->relhasrowsecurity = 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 8200d98..7087620 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/rowsecurity.h" #include "parser/parse_relation.h" +#include "parser/parsetree.h" #include "rewrite/rewriteHandler.h" #include "storage/fd.h" #include "tcop/tcopprot.h" #include "utils/acl.h" #include "utils/builtins.h" #include "utils/lsyscache.h" +#include "utils/syscache.h" #include "utils/memutils.h" #include "utils/portal.h" #include "utils/rel.h" @@ -744,7 +749,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; @@ -774,14 +779,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_security_policy(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) - @@ -793,6 +810,7 @@ DoCopy(const CopyStmt *stmt, const char *queryString) rte->selectedCols = bms_add_member(rte->selectedCols, attno); } ExecCheckRTPerms(list_make1(rte), true); + } } else { @@ -1149,6 +1167,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 @@ -1220,6 +1285,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_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, @@ -1244,6 +1328,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 && @@ -2024,6 +2109,7 @@ CopyFrom(CopyState cstate) palloc0(resultRelInfo->ri_TrigDesc->numtriggers * sizeof(List *)); } resultRelInfo->ri_TrigInstrument = NULL; + resultrel_row_security_init(CMD_INSERT, resultRelInfo); ExecOpenIndices(resultRelInfo); @@ -2136,7 +2222,7 @@ CopyFrom(CopyState cstate) if (!skip_tuple) { /* Check the constraints of the tuple */ - if (cstate->rel->rd_att->constr) + if (cstate->rel->rd_att->constr || resultRelInfo->ri_rowSecurity) ExecConstraints(resultRelInfo, slot, estate); if (useHeapMultiInsert) 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 4065740..dfa573c 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_rowsecurity.h" #include "catalog/pg_tablespace.h" #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" @@ -2739,6 +2740,8 @@ AlterTableGetLockLevel(List *cmds) case AT_SetTableSpace: /* must rewrite heap */ case AT_DropNotNull: /* may change some SQL plans */ case AT_SetNotNull: + case AT_SetRowSecurity: + case AT_ResetRowSecurity: case AT_GenericOptions: case AT_AlterColumnGenericOptions: cmd_lockmode = AccessExclusiveLock; @@ -3102,6 +3105,8 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd, case AT_DropInherit: /* NO INHERIT */ case AT_AddOf: /* OF */ case AT_DropOf: /* NOT OF */ + case AT_SetRowSecurity: + case AT_ResetRowSecurity: ATSimplePermissions(rel, ATT_TABLE); /* These commands never recurse */ /* No command-specific prep needed */ @@ -3381,6 +3386,12 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab, Relation rel, case AT_DropOf: ATExecDropOf(rel, lockmode); break; + case AT_SetRowSecurity: + ATExecSetRowSecurity(rel, (Node *) cmd->def); + break; + case AT_ResetRowSecurity: + ATExecSetRowSecurity(rel, NULL); + break; case AT_GenericOptions: ATExecGenericOptions(rel, (List *) cmd->def); break; @@ -7546,6 +7557,22 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, Assert(defaultexpr); break; + case OCLASS_ROWSECURITY: + /* + * A row-level security policy can depend on a column in case + * when the policy clause references a particular column. + * Due to same reason why TRIGGER ... WHEN does not support + * to change column's type being referenced in clause, row- + * level security policy also does not support it. + */ + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type of a column used in a row-level security policy"), + errdetail("%s depends on column \"%s\"", + getObjectDescription(&foundObject), + colName))); + break; + case OCLASS_PROC: case OCLASS_TYPE: case OCLASS_CAST: diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index 0222d40..c1c4108 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -42,11 +42,14 @@ #include "access/transam.h" #include "access/xact.h" #include "catalog/namespace.h" +#include "catalog/pg_type.h" #include "commands/trigger.h" #include "executor/execdebug.h" +#include "executor/tstoreReceiver.h" #include "mb/pg_wchar.h" #include "miscadmin.h" #include "optimizer/clauses.h" +#include "optimizer/rowsecurity.h" #include "parser/parsetree.h" #include "storage/bufmgr.h" #include "storage/lmgr.h" @@ -54,8 +57,10 @@ #include "utils/acl.h" #include "utils/lsyscache.h" #include "utils/memutils.h" +#include "utils/portal.h" #include "utils/snapmgr.h" #include "utils/tqual.h" +#include "tcop/pquery.h" /* Hooks for plugins to get control in ExecutorStart/Run/Finish/End */ @@ -757,6 +762,7 @@ InitPlan(QueryDesc *queryDesc, int eflags) resultRelation, resultRelationIndex, estate->es_instrument); + resultrel_row_security_init(operation, resultRelInfo); resultRelInfo++; } estate->es_result_relations = resultRelInfos; @@ -1443,6 +1449,98 @@ ExecutePlan(EState *estate, } } +/* + * ExecRelSubPlanCheck - check that tuple meets row-security policy that + * contains sub-query running on other relation + */ +static bool +ExecRelSubPlanCheck(ResultRelInfo *resultRelInfo, TupleTableSlot *scanslot) +{ + ParamListInfo paramLI = NULL; + TupleDesc tupdesc; + Portal portal; + DestReceiver *receiver; + TupleTableSlot *slot = MakeTupleTableSlot(); + Datum value; + bool isnull; + bool result = false; + + Assert(IsA(resultRelInfo->ri_rowSecurity, PlannedStmt)); + + /* Stack fields of the fetched tuple as query parameters. */ + if (resultRelInfo->ri_rowSecParams != NIL) + { + TupleDesc tupdesc = scanslot->tts_tupleDescriptor; + ListCell *cell; + int i = 0; + + paramLI = palloc0(sizeof(ParamListInfoData) + + sizeof(ParamExternData) * + list_length(resultRelInfo->ri_rowSecParams)); + paramLI->numParams = list_length(resultRelInfo->ri_rowSecParams); + + foreach (cell, resultRelInfo->ri_rowSecParams) + { + AttrNumber attno = lfirst_int(cell); + + value = slot_getattr(scanslot, attno, &isnull); + paramLI->params[i].value = value; + paramLI->params[i].isnull = isnull; + paramLI->params[i].pflags = PARAM_FLAG_CONST; + paramLI->params[i].ptype = tupdesc->attrs[attno - 1]->atttypid; + i++; + } + } + /* Create tuple descriptor */ + tupdesc = CreateTemplateTupleDesc(1, false); + TupleDescInitEntry(tupdesc, (AttrNumber) 1, "result", + BOOLOID, -1, 0); + ExecSetSlotDescriptor(slot, tupdesc); + + /* Create a new temporary portal to run the query in */ + portal = CreateNewPortal(); + portal->visible = false; + PortalCreateHoldStore(portal); + + /* Create DestReceiver of Tuplestore */ + receiver = CreateDestReceiver(DestTuplestore); + SetTuplestoreDestReceiverParams(receiver, + portal->holdStore, + portal->holdContext, + false); + PortalDefineQuery(portal, + NULL, + "(query not available)", + "SELECT", + list_make1(resultRelInfo->ri_rowSecurity), + NULL); + /* Run query */ + PortalStart(portal, paramLI, 0, GetActiveSnapshot()); + + (void) PortalRun(portal, + FETCH_ALL, + false, + receiver, + receiver, + NULL); + + /* And fetch a tuple for result */ + if (tuplestore_gettupleslot(portal->holdStore, true, false, slot)) + { + if (TupIsNull(slot)) + elog(ERROR, "row-security policy returns empty tuple"); + + value = slot_getattr(slot, 1, &isnull); + if (!isnull) + result = DatumGetBool(value); + } + + (*receiver->rDestroy) (receiver); + + PortalDrop(portal, false); + + return result; +} /* * ExecRelCheck --- check that tuple meets constraints for result relation @@ -1452,13 +1550,20 @@ ExecRelCheck(ResultRelInfo *resultRelInfo, TupleTableSlot *slot, EState *estate) { Relation rel = resultRelInfo->ri_RelationDesc; - int ncheck = rel->rd_att->constr->num_check; - ConstrCheck *check = rel->rd_att->constr->check; + TupleConstr *constr = rel->rd_att->constr; + int ncheck = 0; ExprContext *econtext; MemoryContext oldContext; List *qual; int i; + /* calculate number of checks */ + if (constr) + ncheck += constr->num_check; + if (resultRelInfo->ri_rowSecurity && + !IsA(resultRelInfo->ri_rowSecurity, PlannedStmt)) + ncheck ++; + /* * If first time through for this result relation, build expression * nodetrees for rel's constraint expressions. Keep them in the per-query @@ -1469,12 +1574,24 @@ ExecRelCheck(ResultRelInfo *resultRelInfo, oldContext = MemoryContextSwitchTo(estate->es_query_cxt); resultRelInfo->ri_ConstraintExprs = (List **) palloc(ncheck * sizeof(List *)); - for (i = 0; i < ncheck; i++) + if (constr) + { + ConstrCheck *check = constr->check; + + for (i = 0; i < constr->num_check; i++) + { + /* ExecQual wants implicit-AND form */ + qual = make_ands_implicit(stringToNode(check[i].ccbin)); + resultRelInfo->ri_ConstraintExprs[i] = (List *) + ExecPrepareExpr((Expr *) qual, estate); + } + } + if (resultRelInfo->ri_rowSecurity && + !IsA(resultRelInfo->ri_rowSecurity, PlannedStmt)) { - /* ExecQual wants implicit-AND form */ - qual = make_ands_implicit(stringToNode(check[i].ccbin)); - resultRelInfo->ri_ConstraintExprs[i] = (List *) - ExecPrepareExpr((Expr *) qual, estate); + qual = make_ands_implicit((Expr *) resultRelInfo->ri_rowSecurity); + resultRelInfo->ri_ConstraintExprs[ncheck - 1] = + (List *) ExecPrepareExpr((Expr *) qual, estate); } MemoryContextSwitchTo(oldContext); } @@ -1499,9 +1616,31 @@ ExecRelCheck(ResultRelInfo *resultRelInfo, * ExecQual to return TRUE for NULL. */ if (!ExecQual(qual, econtext, true)) - return check[i].ccname; + { + if (constr && i < constr->num_check) + return constr->check[i].ccname; + + /* elsewhere, it is violation towards row-security policy */ + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("new row for relation \"%s\" violates row-secirity", + RelationGetRelationName(rel)), + errdetail("Failing row contains %s.", + ExecBuildSlotValueDescription(slot, 64)))); + } } + /* And evaluate the row-security policy if it has SubPlan */ + if (resultRelInfo->ri_rowSecurity && + IsA(resultRelInfo->ri_rowSecurity, PlannedStmt) && + !ExecRelSubPlanCheck(resultRelInfo, slot)) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("new row for relation \"%s\" violates row-secirity", + RelationGetRelationName(rel)), + errdetail("Failing row contains %s.", + ExecBuildSlotValueDescription(slot, 64)))); + /* NULL result means no error */ return NULL; } @@ -1513,9 +1652,7 @@ ExecConstraints(ResultRelInfo *resultRelInfo, Relation rel = resultRelInfo->ri_RelationDesc; TupleConstr *constr = rel->rd_att->constr; - Assert(constr); - - if (constr->has_not_null) + if (constr && constr->has_not_null) { int natts = rel->rd_att->natts; int attrChk; @@ -1533,7 +1670,7 @@ ExecConstraints(ResultRelInfo *resultRelInfo, } } - if (constr->num_check > 0) + if ((constr && constr->num_check > 0) || resultRelInfo->ri_rowSecurity) { const char *failed; diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c index d31015c..a8ffbcb 100644 --- a/src/backend/executor/nodeModifyTable.c +++ b/src/backend/executor/nodeModifyTable.c @@ -230,7 +230,8 @@ ExecInsert(TupleTableSlot *slot, /* * Check the constraints of the tuple */ - if (resultRelationDesc->rd_att->constr) + if (resultRelationDesc->rd_att->constr || + resultRelInfo->ri_rowSecurity) ExecConstraints(resultRelInfo, slot, estate); /* @@ -579,7 +580,8 @@ ExecUpdate(ItemPointer tupleid, * so there's no need to do them again.) */ lreplace:; - if (resultRelationDesc->rd_att->constr) + if (resultRelationDesc->rd_att->constr || + resultRelInfo->ri_rowSecurity) ExecConstraints(resultRelInfo, slot, estate); /* 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..494bed9 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/rowsecurity.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-security policy of the relation being referenced, + * if configured with either of built-in or extension's features. + * RangeTblEntry of the relation with row-security policy shall + * be replaced with a row-security subquery that has simple scan + * on the target relation with row-security policy qualifiers. + * + * This routine assumes PlannerInfo is already handled with + * expand_inherited_tables, thus, AppendRelInfo or PlanRowMark + * have valid information. + */ + apply_row_security_policy(root); + + /* * Set hasHavingQual to remember if HAVING clause is present. Needed * because preprocess_expression will reduce a constant-true condition to * an empty qual list ... but "HAVING TRUE" is not a semantic no-op. @@ -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_SECURITY) { Index newrti; diff --git a/src/backend/optimizer/prep/preptlist.c b/src/backend/optimizer/prep/preptlist.c index 1af4e7f..6eea45f 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_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_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..7db20de 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_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_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_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..3f5cb19 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 rowsecurity.o include $(top_srcdir)/src/backend/common.mk diff --git a/src/backend/optimizer/util/rowsecurity.c b/src/backend/optimizer/util/rowsecurity.c new file mode 100644 index 0000000..da39206 --- /dev/null +++ b/src/backend/optimizer/util/rowsecurity.c @@ -0,0 +1,845 @@ +/* + * optimizer/util/rowsecurity.c + * Routines to support row-security feature + * + * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + */ +#include "postgres.h" + +#include "access/heapam.h" +#include "access/htup_details.h" +#include "access/sysattr.h" +#include "catalog/pg_class.h" +#include "catalog/pg_inherits_fn.h" +#include "catalog/pg_rowsecurity.h" +#include "catalog/pg_type.h" +#include "miscadmin.h" +#include "nodes/makefuncs.h" +#include "nodes/nodeFuncs.h" +#include "nodes/plannodes.h" +#include "optimizer/clauses.h" +#include "optimizer/prep.h" +#include "optimizer/rowsecurity.h" +#include "parser/parsetree.h" +#include "rewrite/rewriteHandler.h" +#include "utils/lsyscache.h" +#include "utils/rel.h" +#include "utils/syscache.h" +#include "tcop/utility.h" + +/* flags to pull row-security policy */ +#define RSEC_FLAG_HAS_SUBLINKS 0x0001 + +/* hook to allow extensions to apply their own security policy */ +row_security_policy_hook_type row_security_policy_hook = NULL; + +/* + * make_pseudo_column + * + * It makes 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 row-security subquery. 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_SECURITY); + + foreach (cell, subqry->targetList) + { + subtle = lfirst(cell); + + /* + * If referenced system column is already attached on the target- + * list of row-security subquery, 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 row-security subquery, 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_SECURITY) + { + /* + * If this Var node is system-column or whole-row reference + * on row-security subqueries, 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 row-security subqueries, 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_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-security configuration, if we try to expand + * the relation inside of row-security subquery originated from + * same relation! + */ +static void +check_infinite_recursion(PlannerInfo *root, Oid relid) +{ + PlannerInfo *parent = root->parent_root; + + if (parent && parent->parse->querySource == QSRC_ROW_SECURITY) + { + RangeTblEntry *rte = rt_fetch(1, parent->parse->rtable); + + Assert(rte->rtekind == RTE_RELATION && OidIsValid(rte->relid)); + + if (relid == rte->relid) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("infinite recursion detected for relation \"%s\"", + get_rel_name(relid)))); + check_infinite_recursion(parent, relid); + } +} + +/* + * fixup_plan_rowmark + * + * Push down the given PlanRowMark into row-security subquery. + */ +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_with_policy + * + * It replaces the supplied RangeTblEntry (should be RTE_RELATION) by row- + * security subquery with configured row-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_with_policy(PlannerInfo *root, Index rtindex, + Expr *qual, int flags) +{ + Query *parse = root->parse; + RangeTblEntry *rte = rt_fetch(rtindex, parse->rtable); + Query *subqry; + RangeTblEntry *subrte; + RangeTblRef *subrtr; + TargetEntry *subtle; + 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 & RSEC_FLAG_HAS_SUBLINKS) + QueryRewriteExpr((Node *)qual, list_make1_oid(rte->relid)); + + /* + * Construction of sub-query + */ + subqry = (Query *) makeNode(Query); + subqry->commandType = CMD_SELECT; + subqry->querySource = QSRC_ROW_SECURITY; + + subrte = copyObject(rte); + subqry->rtable = list_make1(subrte); + + subrtr = makeNode(RangeTblRef); + subrtr->rtindex = 1; + subqry->jointree = makeFromExpr(list_make1(subrtr), (Node *) qual); + if (flags & RSEC_FLAG_HAS_SUBLINKS) + subqry->hasSubLinks = true; + + /* + * 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_security_policy + * + * It pulls the configured row-security policy of both built-in and + * extensions. If any, it returns expression tree. + */ +static Expr * +pull_row_security_policy(CmdType cmd, Relation relation, int *p_flags) +{ + Expr *quals = NULL; + int flags = 0; + + /* + * Pull the row-security policy configured with built-in features, + * if unprivileged users. Please note that superuser can bypass it. + */ + if (relation->rsdesc && !superuser()) + { + RowSecurityDesc *rsdesc = relation->rsdesc; + + quals = copyObject(rsdesc->rsqual); + if (rsdesc->rshassublinks) + flags |= RSEC_FLAG_HAS_SUBLINKS; + } + + /* + * Also, ask extensions whether they want to apply their own + * row-security policy. If both built-in and extension has + * their own policy, it shall be merged. + */ + if (row_security_policy_hook) + { + List *temp; + + temp = (*row_security_policy_hook)(cmd, relation); + if (temp != NIL) + { + if ((flags & RSEC_FLAG_HAS_SUBLINKS) == 0 && + contain_subplans((Node *) temp)) + flags |= RSEC_FLAG_HAS_SUBLINKS; + + if (quals != NULL) + temp = lappend(temp, quals); + + if (list_length(temp) == 1) + quals = (Expr *)list_head(temp); + else if (list_length(temp) > 1) + quals = makeBoolExpr(AND_EXPR, temp, -1); + } + } + *p_flags = flags; + return quals; +} + +/* + * copy_row_security_policy + * + * It construct a row-security subquery instead of raw COPY TO statement, + * if target relation has a row-level security policy + */ +bool +copy_row_security_policy(CopyStmt *stmt, Relation rel, List *attnums) +{ + Expr *quals; + int flags; + Query *parse; + RangeTblEntry *rte; + RangeTblRef *rtr; + TargetEntry *tle; + Var *var; + ListCell *cell; + + if (stmt->is_from) + return false; + + quals = pull_row_security_policy(CMD_SELECT, rel, &flags); + if (!quals) + return false; + + parse = (Query *) makeNode(Query); + parse->commandType = CMD_SELECT; + parse->querySource = QSRC_ROW_SECURITY; + + rte = makeNode(RangeTblEntry); + rte->rtekind = RTE_RELATION; + rte->relid = RelationGetRelid(rel); + rte->relkind = RelationGetForm(rel)->relkind; + + foreach (cell, attnums) + { + HeapTuple tuple; + Form_pg_attribute attform; + AttrNumber attno = lfirst_int(cell); + + tuple = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(RelationGetRelid(rel)), + Int16GetDatum(attno)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for attribute %d of relation %s", + attno, RelationGetRelationName(rel)); + attform = (Form_pg_attribute) GETSTRUCT(tuple); + + var = makeVar((Index) 1, + attform->attnum, + attform->atttypid, + attform->atttypmod, + attform->attcollation, + 0); + tle = makeTargetEntry((Expr *) var, + list_length(parse->targetList) + 1, + pstrdup(NameStr(attform->attname)), + false); + parse->targetList = lappend(parse->targetList, tle); + + ReleaseSysCache(tuple); + + rte->selectedCols = bms_add_member(rte->selectedCols, + attno - FirstLowInvalidHeapAttributeNumber); + } + rte->inFromCl = true; + rte->requiredPerms = ACL_SELECT; + + rtr = makeNode(RangeTblRef); + rtr->rtindex = 1; + + parse->jointree = makeFromExpr(list_make1(rtr), (Node *) quals); + parse->rtable = list_make1(rte); + if (flags & RSEC_FLAG_HAS_SUBLINKS) + parse->hasSubLinks = true; + + stmt->query = (Node *) parse; + + return true; +} + +/* + * apply_row_security_policy + * + * Entrypoint to apply configured row-security policy of the relation. + * + * In case when the supplied query references relations with row-security + * policy, its RangeTblEntry shall be replaced by a row-security subquery + * that has simple scan on the referenced table with policy qualifiers. + * Of course, security-barrier shall be set on the subquery to prevent + * unexpected push-down of functions without leakproof flag. + * + * For example, when table t1 has a security policy "(x % 2 = 0)", the + * following query: + * SELECT * FROM t1 WHERE f_leak(y) + * performs as if + * SELECT * FROM ( + * SELECT x, y FROM t1 WHERE (x % 2 = 0) + * ) AS t1 WHERE f_leak(y) + * would be given. Because the sub-query has security barrier flag, + * configured security policy qualifier is always executed prior to + * user given functions. + */ +void +apply_row_security_policy(PlannerInfo *root) +{ + Query *parse = root->parse; + Oid curr_userid; + int curr_seccxt; + Index rtindex; + ListCell *cell; + bool has_row_security_policy = 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); + CmdType cmd = CMD_SELECT; + Relation rel; + Expr *quals; + int flags = 0; + + /* 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 try applying row-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_SECURITY && + rtindex == 1) + continue; + + /* + * Is it a result relation of UPDATE or DELETE command? + */ + if (parse->commandType != CMD_SELECT) + { + if (parse->resultRelation == rtindex) + cmd = parse->commandType; + else + { + foreach (cell, root->append_rel_list) + { + AppendRelInfo *apinfo = lfirst(cell); + + if (apinfo->parent_relid == parse->resultRelation && + apinfo->child_relid == rtindex) + { + cmd = parse->commandType; + break; + } + } + } + } + + /* + * 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_security_policy(cmd, rel, &flags); + if (quals) + { + expand_rtentry_with_policy(root, rtindex, quals, flags); + has_row_security_policy = true; + } + heap_close(rel, NoLock); + } + + if (has_row_security_policy) + { + 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 row-security policy), thus, it has to be invalidated + * when its assumption was changed. + */ + if (!OidIsValid(glob->planUserId)) + { + /* Plan invalidation on session user-id */ + glob->planUserId = GetUserId(); + + /* Plan invalidation on catalog updates of pg_authid */ + pi_item = makeNode(PlanInvalItem); + pi_item->cacheId = AUTHOID; + pi_item->hashValue = + GetSysCacheHashValue1(AUTHOID, + ObjectIdGetDatum(glob->planUserId)); + glob->invalItems = lappend(glob->invalItems, pi_item); + } + else + Assert(glob->planUserId == GetUserId()); + + /* + * XXX - varattno of Var node that references the RangeTblEntry + * being replaced by row-security subquery 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); + } +} + +/* + * replace_varnode_mutator + * + * Contents of the newer tuple shall be delivered as parameters, in case + * when row-security policy needs to be run as secondary query. This + * mutator replaces Var node that references the newer tuple with Param + * node, and constructs a list to hold attribute numbers to be delivered. + */ +typedef struct { + int varlevelsup; + List *varparams; +} replace_varnode_context; + +static Node * +replace_varnode_mutator(Node *node, replace_varnode_context *context) +{ + if (node == NULL) + return NULL; + else if (IsA(node, Var)) + { + Var *var = (Var *) node; + Param *param; + ListCell *cell; + + if (var->varlevelsup == context->varlevelsup) + { + int paramid = 1; + + Assert(var->varno == 1); + + /* + * Is this Var node already appeared in varparams list? + * If not, needs to be added. + */ + foreach (cell, context->varparams) + { + if (lfirst_int(cell) == var->varattno) + break; + paramid++; + } + if (!cell) + { + context->varparams = lappend_int(context->varparams, + var->varattno); + paramid = list_length(context->varparams); + } + + /* Param is used to reference a particular field of new tuple */ + param = makeNode(Param); + param->paramkind = PARAM_EXTERN; + param->paramid = paramid; + param->paramtype = var->vartype; + param->paramtypmod = var->vartypmod; + param->paramcollid = var->varcollid; + param->location = -1; + + return (Node *)param; + } + } + else if (IsA(node, SubLink)) + { + Node *result; + + context->varlevelsup++; + result = expression_tree_mutator(node, + replace_varnode_mutator, + (void *)context); + context->varlevelsup--; + + return result; + } + return expression_tree_mutator(node, replace_varnode_mutator, + (void *)context); +} + +/* + * resultrel_row_security_init + * + * It pulls row-security policy of the result relation to be checked + * just before row INSERT or UPDATE. Also, it constructs PlannedStmt + * if row-security policy needs secondary query to check privileges + * to do. Either an expression tree or PlannedStmt shall be stored in + * the ResultRelInfo->ri_rowSecurity. Elsewhere, it is NULL. + */ +void +resultrel_row_security_init(CmdType cmd, ResultRelInfo *relinfo) +{ + Relation relation = relinfo->ri_RelationDesc; + Node *qual; + int flags = 0; + + qual = (Node *) pull_row_security_policy(cmd, relation, &flags); + + /* + * In case when row-security policy contains SubLinks, it is not + * available to run the qualifier using ExecQual. This case needs + * to run sub-query for each rows using ExecRelSubPlanCheck(), so + * we set up PlannedStmt of the following simple query. + * SELECT (row security policy); + */ + if (flags & RSEC_FLAG_HAS_SUBLINKS) + { + Query *subqry; + TargetEntry *subtle; + replace_varnode_context context; + + /* replace varnode into parameter reference */ + memset(&context, 0, sizeof(context)); + qual = expression_tree_mutator(qual, + replace_varnode_mutator, + (void *) &context); + if (exprType(qual) != BOOLOID) + elog(ERROR, "unsupported row-security policy data type"); + relinfo->ri_rowSecParams = context.varparams; + + /* setup dummy subquery */ + subtle = makeTargetEntry((Expr *) qual, 1, "result", false); + subqry = makeNode(Query); + subqry->commandType = CMD_SELECT; + subqry->querySource = QSRC_ORIGINAL; + subqry->targetList = list_make1(subtle); + subqry->jointree = makeFromExpr(NIL, NULL); + subqry->hasSubLinks = true; + + /* construct PlannedStmt */ + qual = (Node *) pg_plan_query(subqry, 0, NULL); + } + else + relinfo->ri_rowSecParams = NIL; + + relinfo->ri_rowSecurity = qual; +} diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index ad98b36..42dd7f8 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 SECURITY (expression) */ + | SET ROW SECURITY '(' a_expr ')' + { + AlterTableCmd *n = makeNode(AlterTableCmd); + n->subtype = AT_SetRowSecurity; + n->def = (Node *) $5; + $$ = (Node *)n; + } + /* ALTER TABLE RESET ROW SECURITY */ + | RESET ROW SECURITY + { + AlterTableCmd *n = makeNode(AlterTableCmd); + n->subtype = AT_ResetRowSecurity; + 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..9b742d3 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_SECURITY: + err = _("aggregate functions are not allowed in row-security policy"); + 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_SECURITY: + err = _("window functions are not allowed in row-security policy"); + break; /* * There is intentionally no default: case here, so that the diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index e9267c5..fce2d2b 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_SECURITY: /* 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_SECURITY: + return "ROW SECURITY"; /* * There is intentionally no default: case here, so that the diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c index 990ca34..b802eec 100644 --- a/src/backend/rewrite/rewriteHandler.c +++ b/src/backend/rewrite/rewriteHandler.c @@ -2914,3 +2914,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 9a504f8..284911b 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_rowsecurity.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->relhasrowsecurity) + RelationBuildRowSecurity(relation); + else + relation->rsdesc = NULL; + /* * if it's an index, initialize index-related information */ @@ -1799,6 +1805,8 @@ RelationDestroyRelation(Relation relation) MemoryContextDelete(relation->rd_indexcxt); if (relation->rd_rulescxt) MemoryContextDelete(relation->rd_rulescxt); + if (relation->rsdesc) + MemoryContextDelete(relation->rsdesc->rscxt); pfree(relation); } @@ -3038,7 +3046,13 @@ RelationCacheInitializePhase3(void) relation->rd_rel->relhastriggers = false; restart = true; } - + if (relation->rd_rel->relhasrowsecurity && relation->rsdesc == NULL) + { + RelationBuildRowSecurity(relation); + if (relation->rsdesc == NULL) + relation->rd_rel->relhasrowsecurity = false; + restart = true; + } /* Release hold on the relation */ RelationDecrementReferenceCount(relation); @@ -4191,6 +4205,7 @@ load_relcache_init_file(bool shared) rel->rd_rules = NULL; rel->rd_rulescxt = NULL; rel->trigdesc = NULL; + rel->rsdesc = NULL; rel->rd_indexprs = NIL; rel->rd_indpred = NIL; rel->rd_exclops = NULL; diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index a3466a5..94b823f 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -3926,6 +3926,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"); @@ -3950,7 +3951,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 @@ -3970,7 +4009,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 " @@ -4006,7 +4046,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 " @@ -4041,7 +4082,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 " @@ -4076,7 +4118,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 " @@ -4112,7 +4155,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 " @@ -4147,7 +4191,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 " @@ -4178,7 +4223,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", @@ -4204,7 +4250,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", @@ -4240,7 +4287,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", @@ -4288,6 +4336,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) { @@ -4330,6 +4379,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)) { @@ -12908,6 +12961,9 @@ dumpTableSchema(Archive *fout, TableInfo *tbinfo) } } } + if (tbinfo->rlsqual) + appendPQExpBuffer(q, "ALTER TABLE ONLY %s SET ROW 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 aa1546a..0232073 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -258,6 +258,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/bin/psql/describe.c b/src/bin/psql/describe.c index a1c3158..904d6e8 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -2652,6 +2652,10 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys appendPQExpBuffer(&buf, ",\n pg_catalog.obj_description(c.oid, 'pg_class') as \"%s\"", gettext_noop("Description")); + if (pset.sversion >= 90300) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_get_expr(rs.rsecqual, c.oid) as \"%s\"", + gettext_noop("Row-security")); } appendPQExpBuffer(&buf, @@ -2661,6 +2665,9 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys appendPQExpBuffer(&buf, "\n LEFT JOIN pg_catalog.pg_index i ON i.indexrelid = c.oid" "\n LEFT JOIN pg_catalog.pg_class c2 ON i.indrelid = c2.oid"); + if (verbose && pset.sversion >= 90300) + appendPQExpBuffer(&buf, + "\n LEFT JOIN pg_rowsecurity rs ON rs.rsecrelid = c.oid"); appendPQExpBuffer(&buf, "\nWHERE c.relkind IN ("); if (showTables) diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h index 8499768..a412139 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_ROWSECURITY, /* pg_rowsecurity */ MAX_OCLASS /* MUST BE LAST */ } ObjectClass; diff --git a/src/include/catalog/indexing.h b/src/include/catalog/indexing.h index 238fe58..9331382 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_rowsecurity_relid_index, 3839, on pg_rowsecurity using btree(rsecrelid oid_ops)); +#define RowSecurityIndexId 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..ab25be1 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 relhasrowsecurity; /* has (or has had) row-security policy */ 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_relhasrowsecurity 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_rowsecurity.h b/src/include/catalog/pg_rowsecurity.h new file mode 100644 index 0000000..4796140 --- /dev/null +++ b/src/include/catalog/pg_rowsecurity.h @@ -0,0 +1,58 @@ +/* + * pg_rowsecurity.h + * definition of the system catalog for row-security policy (pg_rowsecurity) + * + * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + */ +#ifndef PG_ROWSECURITY_H +#define PG_ROWSECURITY_H + +#include "catalog/genbki.h" +#include "nodes/primnodes.h" +#include "utils/memutils.h" +#include "utils/relcache.h" + +/* ---------------- + * pg_rowlevelsec definition. cpp turns this into + * typedef struct FormData_pg_rowlevelsec + * ---------------- + */ +#define RowSecurityRelationId 3838 + +CATALOG(pg_rowsecurity,3838) BKI_WITHOUT_OIDS +{ + Oid rsecrelid; +#ifdef CATALOG_VARLEN + pg_node_tree rsecqual; +#endif +} FormData_pg_rowsecurity; + +/* ---------------- + * Form_pg_rowlevelsec corresponds to a pointer to a row with + * the format of pg_rowlevelsec relation. + * ---------------- + */ +typedef FormData_pg_rowsecurity *Form_pg_rowsecurity; + +/* ---------------- + * compiler constants for pg_rowlevelsec + * ---------------- + */ +#define Natts_pg_rowsecurity 2 +#define Anum_pg_rowsecurity_rsecrelid 1 +#define Anum_pg_rowsecurity_rsecqual 2 + +typedef struct +{ + MemoryContext rscxt; + Expr *rsqual; + bool rshassublinks; +} RowSecurityDesc; + +extern void RelationBuildRowSecurity(Relation relation); +extern void ATExecSetRowSecurity(Relation relation, Node *clause); +extern void RemoveRowSecurityById(Oid relationId); + +#endif /* PG_ROWSECURITY_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 b41227f..0bab2c7 100644 --- a/src/include/miscadmin.h +++ b/src/include/miscadmin.h @@ -278,6 +278,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/execnodes.h b/src/include/nodes/execnodes.h index d4911bd..c8a4d11 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -303,6 +303,8 @@ typedef struct JunkFilter * ConstraintExprs array of constraint-checking expr states * junkFilter for removing junk attributes from tuples * projectReturning for computing a RETURNING list + * rowSecurity for row-security checks + * rowSecParams param-list if row-security has SubLink * ---------------- */ typedef struct ResultRelInfo @@ -320,6 +322,8 @@ typedef struct ResultRelInfo List **ri_ConstraintExprs; JunkFilter *ri_junkFilter; ProjectionInfo *ri_projectReturning; + Node *ri_rowSecurity; + List *ri_rowSecParams; } ResultRelInfo; /* ---------------- diff --git a/src/include/nodes/nodeFuncs.h b/src/include/nodes/nodeFuncs.h 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..973782c 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_SECURITY, /* added by row-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_SetRowSecurity, /* SET ROW SECURITY (...) */ + AT_ResetRowSecurity, /* RESET ROW SECURITY */ AT_GenericOptions /* OPTIONS (...) */ } AlterTableType; diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h 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/rowsecurity.h b/src/include/optimizer/rowsecurity.h new file mode 100644 index 0000000..76a04a0 --- /dev/null +++ b/src/include/optimizer/rowsecurity.h @@ -0,0 +1,29 @@ +/* ------------------------------------------------------------------------- + * + * rowsecurity.h + * prototypes for optimizer/rowsecurity.c + * + * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * ------------------------------------------------------------------------- + */ +#ifndef ROWSECURITY_H +#define ROWSECURITY_H + +#include "nodes/execnodes.h" +#include "nodes/parsenodes.h" +#include "nodes/relation.h" +#include "utils/rel.h" + +typedef List *(*row_security_policy_hook_type)(CmdType cmdtype, + Relation relation); +extern PGDLLIMPORT row_security_policy_hook_type row_security_policy_hook; + +extern bool copy_row_security_policy(CopyStmt *stmt, + Relation relation, List *attnums); +extern void apply_row_security_policy(PlannerInfo *root); + +extern void resultrel_row_security_init(CmdType cmd, ResultRelInfo *relinfo); + +#endif /* ROWSECURITY_H */ diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h index aa9c648..5ae933d 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_SECURITY, /* ROW SECURITY policy for a table */ } ParseExprKind; diff --git a/src/include/rewrite/rewriteHandler.h b/src/include/rewrite/rewriteHandler.h index 3540e1b..25e3039 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..03b44f2 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_rowsecurity.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 */ + RowSecurityDesc *rsdesc; /* Row-security policy, or NULL */ /* * rd_options is set whenever rd_rel is loaded into the relcache entry. diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out new file mode 100644 index 0000000..448a459 --- /dev/null +++ b/src/test/regress/expected/rowsecurity.out @@ -0,0 +1,1007 @@ +-- +-- Test of Row-level security feature +-- +-- Clean up in case a prior regression run failed +-- Suppress NOTICE messages when users/groups don't exist +SET client_min_messages TO 'warning'; +DROP USER IF EXISTS rls_regress_user0; +DROP USER IF EXISTS rls_regress_user1; +DROP USER IF EXISTS rls_regress_user2; +DROP SCHEMA IF EXISTS rls_regress_schema CASCADE; +RESET client_min_messages; +-- initial setup +CREATE USER rls_regress_user0; +CREATE USER rls_regress_user1; +CREATE USER rls_regress_user2; +CREATE SCHEMA rls_regress_schema; +GRANT ALL ON SCHEMA rls_regress_schema TO public; +SET search_path = rls_regress_schema; +-- setup of malicious function +CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool + COST 0.0000001 LANGUAGE plpgsql + AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END'; +GRANT EXECUTE ON FUNCTION f_leak(text) TO public; +-- BASIC Row-Level Security Scenario +SET SESSION AUTHORIZATION rls_regress_user0; +CREATE TABLE uaccount ( + pguser name primary key, + seclv int +); +INSERT INTO uaccount VALUES + ('rls_regress_user0', 99), + ('rls_regress_user1', 1), + ('rls_regress_user2', 2), + ('rls_regress_user3', 3); +GRANT SELECT ON uaccount TO public; +CREATE TABLE category ( + cid int primary key, + cname text +); +GRANT ALL ON category TO public; +INSERT INTO category VALUES + (11, 'novel'), + (22, 'science fiction'), + (33, 'technology'), + (44, 'manga'); +CREATE TABLE document ( + did int primary key, + cid int references category(cid), + dlevel int not null, + dauthor name, + dtitle text +); +GRANT ALL ON document TO public; +INSERT INTO document VALUES + ( 1, 11, 1, 'rls_regress_user1', 'my first novel'), + ( 2, 11, 2, 'rls_regress_user1', 'my second novel'), + ( 3, 22, 2, 'rls_regress_user1', 'my science fiction'), + ( 4, 44, 1, 'rls_regress_user1', 'my first manga'), + ( 5, 44, 2, 'rls_regress_user1', 'my second manga'), + ( 6, 22, 1, 'rls_regress_user2', 'great science fiction'), + ( 7, 33, 2, 'rls_regress_user2', 'great technology book'), + ( 8, 44, 1, 'rls_regress_user2', 'great manga'); +-- user's security level must higher than or equal to document's one +ALTER TABLE document SET ROW SECURITY + (dlevel <= (SELECT seclv FROM uaccount WHERE pguser = current_user)); +-- viewpoint from rls_regress_user1 +SET SESSION AUTHORIZATION rls_regress_user1; +SELECT * FROM document WHERE f_leak(dtitle); +NOTICE: f_leak => my first novel +NOTICE: f_leak => my first manga +NOTICE: f_leak => great science fiction +NOTICE: f_leak => great manga + did | cid | dlevel | dauthor | dtitle +-----+-----+--------+-------------------+----------------------- + 1 | 11 | 1 | rls_regress_user1 | my first novel + 4 | 44 | 1 | rls_regress_user1 | my first manga + 6 | 22 | 1 | rls_regress_user2 | great science fiction + 8 | 44 | 1 | rls_regress_user2 | great manga +(4 rows) + +SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle); +NOTICE: f_leak => my first novel +NOTICE: f_leak => my first manga +NOTICE: f_leak => great science fiction +NOTICE: f_leak => great manga + cid | did | dlevel | dauthor | dtitle | cname +-----+-----+--------+-------------------+-----------------------+----------------- + 11 | 1 | 1 | rls_regress_user1 | my first novel | novel + 22 | 6 | 1 | rls_regress_user2 | great science fiction | science fiction + 44 | 8 | 1 | rls_regress_user2 | great manga | manga + 44 | 4 | 1 | rls_regress_user1 | my first manga | manga +(4 rows) + +-- viewpoint from rls_regress_user2 +SET SESSION AUTHORIZATION rls_regress_user2; +SELECT * FROM document WHERE f_leak(dtitle); +NOTICE: f_leak => my first novel +NOTICE: f_leak => my second novel +NOTICE: f_leak => my science fiction +NOTICE: f_leak => my first manga +NOTICE: f_leak => my second manga +NOTICE: f_leak => great science fiction +NOTICE: f_leak => great technology book +NOTICE: f_leak => great manga + did | cid | dlevel | dauthor | dtitle +-----+-----+--------+-------------------+----------------------- + 1 | 11 | 1 | rls_regress_user1 | my first novel + 2 | 11 | 2 | rls_regress_user1 | my second novel + 3 | 22 | 2 | rls_regress_user1 | my science fiction + 4 | 44 | 1 | rls_regress_user1 | my first manga + 5 | 44 | 2 | rls_regress_user1 | my second manga + 6 | 22 | 1 | rls_regress_user2 | great science fiction + 7 | 33 | 2 | rls_regress_user2 | great technology book + 8 | 44 | 1 | rls_regress_user2 | great manga +(8 rows) + +SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle); +NOTICE: f_leak => my first novel +NOTICE: f_leak => my second novel +NOTICE: f_leak => my science fiction +NOTICE: f_leak => my first manga +NOTICE: f_leak => my second manga +NOTICE: f_leak => great science fiction +NOTICE: f_leak => great technology book +NOTICE: f_leak => great manga + cid | did | dlevel | dauthor | dtitle | cname +-----+-----+--------+-------------------+-----------------------+----------------- + 11 | 2 | 2 | rls_regress_user1 | my second novel | novel + 11 | 1 | 1 | rls_regress_user1 | my first novel | novel + 22 | 6 | 1 | rls_regress_user2 | great science fiction | science fiction + 22 | 3 | 2 | rls_regress_user1 | my science fiction | science fiction + 33 | 7 | 2 | rls_regress_user2 | great technology book | technology + 44 | 8 | 1 | rls_regress_user2 | great manga | manga + 44 | 5 | 2 | rls_regress_user1 | my second manga | manga + 44 | 4 | 1 | rls_regress_user1 | my first manga | manga +(8 rows) + +EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle); + QUERY PLAN +---------------------------------------------------------- + Subquery Scan on document + Filter: f_leak(document.dtitle) + -> Seq Scan on document document_1 + Filter: (dlevel <= $0) + InitPlan 1 (returns $0) + -> Index Scan using uaccount_pkey on uaccount + Index Cond: (pguser = "current_user"()) +(7 rows) + +EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle); + QUERY PLAN +---------------------------------------------------------------------- + Hash Join + Hash Cond: (category.cid = document.cid) + -> Seq Scan on category + -> Hash + -> Subquery Scan on document + Filter: f_leak(document.dtitle) + -> Seq Scan on document document_1 + Filter: (dlevel <= $0) + InitPlan 1 (returns $0) + -> Index Scan using uaccount_pkey on uaccount + Index Cond: (pguser = "current_user"()) +(11 rows) + +-- only owner can change row-level security +ALTER TABLE document SET ROW SECURITY (true); -- fail +ERROR: must be owner of relation document +ALTER TABLE document RESET ROW SECURITY; -- fail +ERROR: must be owner of relation document +SET SESSION AUTHORIZATION rls_regress_user0; +ALTER TABLE document SET ROW 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 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 SECURITY (a % 2 = 0); -- be even number +ALTER TABLE t2 SET ROW 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 SECURITY + (a in (select x from s2 where y like '%2f%')); +ALTER TABLE s2 SET ROW 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 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 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 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) + +-- row-security check on row insert / update +INSERT INTO t1(a,b) VALUES (10, 'ABCD'); -- OK +INSERT INTO t1(a,b) VALUES (11, 'EFGI'); -- fail +ERROR: new row for relation "t1" violates row-secirity +DETAIL: Failing row contains (11, null, EFGI). +INSERT INTO t2(a,b) VALUES (11, 'HIJL'); -- OK +UPDATE t1 SET a = a + 2; -- OK +UPDATE t1 SET a = a + 1; -- fail +ERROR: new row for relation "t1" violates row-secirity +DETAIL: Failing row contains (13, null, ABCD). +-- also, in case when row-security contains SubLink node +ALTER TABLE t3 SET ROW SECURITY (t3.a in (SELECT t2.a FROM t2)); +INSERT INTO t3(a,b) VALUES (11, 'foobar'); -- fail +ERROR: new row for relation "t3" violates row-secirity +DETAIL: Failing row contains (null, foobar, 11). +INSERT INTO t3(a,b) VALUES (13, 'foobaz'); -- OK +INSERT INTO t2(a,b) VALUES (11, 'hoge'); -- OK +INSERT INTO t3(a,b) VALUES (11, 'foobar'); -- OK +UPDATE t3 SET a = a + 2; -- fail +ERROR: new row for relation "t3" violates row-secirity +DETAIL: Failing row contains (null, foobaz, 15). +UPDATE t3 SET a = a + 2 WHERE b = 'foobar'; -- OK +-- also, COPY TO command to be dealt as INSERT +COPY t2 FROM stdin; -- fail +ERROR: new row for relation "t2" violates row-secirity +DETAIL: Failing row contains (12, yyy, 120). +CONTEXT: COPY t2, line 2: "12 yyy 120.0" +COPY t2 FROM stdin; -- OK +RESET SESSION AUTHORIZATION; +SELECT tableoid::regclass, * FROM t1; + tableoid | a | b +----------+----+-------- + t1 | 1 | aaa + t1 | 3 | ccc + t1 | 12 | ABCD + t2 | 2 | bcd + t2 | 4 | def + t2 | 13 | HIJL + t2 | 11 | hoge + t2 | 11 | XXX + t2 | 13 | ZZZ + t3 | 13 | foobaz + t3 | 13 | foobar +(11 rows) + +-- +-- Test psql \dt+ command +-- +ALTER TABLE category RESET ROW SECURITY; -- too long qual +\dt+ + List of relations + Schema | Name | Type | Owner | Size | Description | Row-security +--------------------+----------+-------+-------------------+------------+-------------+----------------------------------------------------------- + rls_regress_schema | category | table | rls_regress_user0 | 16 kB | | + rls_regress_schema | document | table | rls_regress_user0 | 16 kB | | (dauthor = "current_user"()) + rls_regress_schema | s1 | table | rls_regress_user0 | 16 kB | | (a IN (SELECT v2.x FROM v2)) + rls_regress_schema | s2 | table | rls_regress_user0 | 16 kB | | (x IN (SELECT s1.a FROM s1 WHERE (s1.b ~~ '%d2%'::text))) + rls_regress_schema | t1 | table | rls_regress_user0 | 16 kB | | ((a % 2) = 0) + rls_regress_schema | t2 | table | rls_regress_user0 | 16 kB | | ((a % 2) = 1) + rls_regress_schema | t3 | table | rls_regress_user0 | 16 kB | | (a IN (SELECT t2.a FROM t2)) + rls_regress_schema | uaccount | table | rls_regress_user0 | 8192 bytes | | +(8 rows) + +-- +-- Clean up objects +-- +RESET SESSION AUTHORIZATION; +DROP SCHEMA rls_regress_schema CASCADE; +NOTICE: drop cascades to 10 other objects +DETAIL: drop cascades to function f_leak(text) +drop cascades to table uaccount +drop cascades to table category +drop cascades to table document +drop cascades to table t1 +drop cascades to table t2 +drop cascades to table t3 +drop cascades to table s1 +drop cascades to table s2 +drop cascades to view v2 +DROP USER rls_regress_user0; +DROP USER rls_regress_user1; +DROP USER rls_regress_user2; diff --git a/src/test/regress/expected/sanity_check.out b/src/test/regress/expected/sanity_check.out index 3f04442..3eb80d2 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_rowsecurity | 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 bdcf3a6..b9fe3bd 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 rowsecurity security_label collate # ---------- # Another group of parallel tests diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule index c7c2ed0..78e6d59 100644 --- a/src/test/regress/serial_schedule +++ b/src/test/regress/serial_schedule @@ -93,6 +93,7 @@ test: delete test: namespace test: prepared_xacts test: privileges +test: rowsecurity test: security_label test: collate test: misc diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql new file mode 100644 index 0000000..35c428a --- /dev/null +++ b/src/test/regress/sql/rowsecurity.sql @@ -0,0 +1,331 @@ +-- +-- Test of Row-level security feature +-- + +-- Clean up in case a prior regression run failed + +-- Suppress NOTICE messages when users/groups don't exist +SET client_min_messages TO 'warning'; + +DROP USER IF EXISTS rls_regress_user0; +DROP USER IF EXISTS rls_regress_user1; +DROP USER IF EXISTS rls_regress_user2; + +DROP SCHEMA IF EXISTS rls_regress_schema CASCADE; + +RESET client_min_messages; + +-- initial setup +CREATE USER rls_regress_user0; +CREATE USER rls_regress_user1; +CREATE USER rls_regress_user2; + +CREATE SCHEMA rls_regress_schema; +GRANT ALL ON SCHEMA rls_regress_schema TO public; +SET search_path = rls_regress_schema; + +-- setup of malicious function +CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool + COST 0.0000001 LANGUAGE plpgsql + AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END'; +GRANT EXECUTE ON FUNCTION f_leak(text) TO public; + +-- BASIC Row-Level Security Scenario + +SET SESSION AUTHORIZATION rls_regress_user0; +CREATE TABLE uaccount ( + pguser name primary key, + seclv int +); +INSERT INTO uaccount VALUES + ('rls_regress_user0', 99), + ('rls_regress_user1', 1), + ('rls_regress_user2', 2), + ('rls_regress_user3', 3); +GRANT SELECT ON uaccount TO public; + +CREATE TABLE category ( + cid int primary key, + cname text +); +GRANT ALL ON category TO public; +INSERT INTO category VALUES + (11, 'novel'), + (22, 'science fiction'), + (33, 'technology'), + (44, 'manga'); + +CREATE TABLE document ( + did int primary key, + cid int references category(cid), + dlevel int not null, + dauthor name, + dtitle text +); +GRANT ALL ON document TO public; +INSERT INTO document VALUES + ( 1, 11, 1, 'rls_regress_user1', 'my first novel'), + ( 2, 11, 2, 'rls_regress_user1', 'my second novel'), + ( 3, 22, 2, 'rls_regress_user1', 'my science fiction'), + ( 4, 44, 1, 'rls_regress_user1', 'my first manga'), + ( 5, 44, 2, 'rls_regress_user1', 'my second manga'), + ( 6, 22, 1, 'rls_regress_user2', 'great science fiction'), + ( 7, 33, 2, 'rls_regress_user2', 'great technology book'), + ( 8, 44, 1, 'rls_regress_user2', 'great manga'); + +-- user's security level must higher than or equal to document's one +ALTER TABLE document SET ROW SECURITY + (dlevel <= (SELECT seclv FROM uaccount WHERE pguser = current_user)); + +-- viewpoint from rls_regress_user1 +SET SESSION AUTHORIZATION rls_regress_user1; +SELECT * FROM document WHERE f_leak(dtitle); +SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle); + +-- viewpoint from rls_regress_user2 +SET SESSION AUTHORIZATION rls_regress_user2; +SELECT * FROM document WHERE f_leak(dtitle); +SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle); + +EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle); +EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle); + +-- only owner can change row-level security +ALTER TABLE document SET ROW SECURITY (true); -- fail +ALTER TABLE document RESET ROW SECURITY; -- fail + +SET SESSION AUTHORIZATION rls_regress_user0; +ALTER TABLE document SET ROW 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 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 SECURITY (a % 2 = 0); -- be even number +ALTER TABLE t2 SET ROW 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 SECURITY + (a in (select x from s2 where y like '%2f%')); + +ALTER TABLE s2 SET ROW 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 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 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 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; + +-- row-security check on row insert / update +INSERT INTO t1(a,b) VALUES (10, 'ABCD'); -- OK +INSERT INTO t1(a,b) VALUES (11, 'EFGI'); -- fail +INSERT INTO t2(a,b) VALUES (11, 'HIJL'); -- OK + +UPDATE t1 SET a = a + 2; -- OK +UPDATE t1 SET a = a + 1; -- fail + +-- also, in case when row-security contains SubLink node +ALTER TABLE t3 SET ROW SECURITY (t3.a in (SELECT t2.a FROM t2)); +INSERT INTO t3(a,b) VALUES (11, 'foobar'); -- fail +INSERT INTO t3(a,b) VALUES (13, 'foobaz'); -- OK +INSERT INTO t2(a,b) VALUES (11, 'hoge'); -- OK +INSERT INTO t3(a,b) VALUES (11, 'foobar'); -- OK + +UPDATE t3 SET a = a + 2; -- fail +UPDATE t3 SET a = a + 2 WHERE b = 'foobar'; -- OK + +-- also, COPY TO command to be dealt as INSERT +COPY t2 FROM stdin; -- fail +11 xxx 110.0 +12 yyy 120.0 +13 zzz 130.0 +\. + +COPY t2 FROM stdin; -- OK +11 XXX 110.0 +13 ZZZ 130.0 +\. + +RESET SESSION AUTHORIZATION; +SELECT tableoid::regclass, * FROM t1; + +-- +-- Test psql \dt+ command +-- +ALTER TABLE category RESET ROW SECURITY; -- too long qual +\dt+ + +-- +-- Clean up objects +-- +RESET SESSION AUTHORIZATION; + +DROP SCHEMA rls_regress_schema CASCADE; + +DROP USER rls_regress_user0; +DROP USER rls_regress_user1; +DROP USER rls_regress_user2;