doc/src/sgml/catalogs.sgml | 59 +++ doc/src/sgml/ref/alter_table.sgml | 38 ++ doc/src/sgml/user-manag.sgml | 123 +++++ src/backend/access/transam/xact.c | 12 + src/backend/catalog/Makefile | 4 +- src/backend/catalog/dependency.c | 21 + src/backend/catalog/heap.c | 1 + src/backend/catalog/pg_rowlevelsec.c | 250 +++++++++ src/backend/commands/tablecmds.c | 64 +++ src/backend/optimizer/plan/planner.c | 17 + src/backend/optimizer/util/Makefile | 2 +- src/backend/optimizer/util/rowlevelsec.c | 677 +++++++++++++++++++++++++ src/backend/parser/gram.y | 16 + src/backend/utils/adt/ri_triggers.c | 14 + src/backend/utils/cache/plancache.c | 32 ++ src/backend/utils/cache/relcache.c | 17 +- src/bin/pg_dump/pg_dump.c | 76 ++- src/bin/pg_dump/pg_dump.h | 1 + src/include/catalog/dependency.h | 1 + src/include/catalog/indexing.h | 3 + src/include/catalog/pg_class.h | 20 +- src/include/catalog/pg_rowlevelsec.h | 60 +++ src/include/nodes/parsenodes.h | 5 +- src/include/nodes/plannodes.h | 2 + src/include/nodes/relation.h | 2 + src/include/optimizer/rowlevelsec.h | 31 ++ src/include/utils/plancache.h | 2 + src/include/utils/rel.h | 2 + src/test/regress/expected/rowlevelsec.out | 753 ++++++++++++++++++++++++++++ src/test/regress/expected/sanity_check.out | 3 +- src/test/regress/parallel_schedule | 2 +- src/test/regress/serial_schedule | 1 + src/test/regress/sql/rowlevelsec.sql | 237 +++++++++ 33 files changed, 2522 insertions(+), 26 deletions(-) diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index fbd6d0d..426274c 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -229,6 +229,11 @@ + pg_rowlevelsec + row-level security policy of relation + + + pg_seclabel security labels on database objects @@ -1802,6 +1807,16 @@ + relhasrowlevelsec + bool + + + True if table has row-level security policy; see + pg_rowlevelsec catalog + + + + relhassubclass bool @@ -4819,6 +4834,50 @@ + + <structname>pg_rowlevelsec</structname> + + + pg_rowlevelsec + + + The catalog pg_rowlevelsec expression tree of + row-level security policy to be performed on a particular relation. + + + <structname>pg_rowlevelsec</structname> Columns + + + + Name + Type + References + Description + + + + + rlsrelid + oid + pg_class.oid + The table this row-level security is for + + + rlsqual + text + + An expression tree to be performed as rowl-level security policy + + + +
+ + + pg_class.relhasrowlevelsec + must be true if a table has row-level security policy in this catalog. + + +
<structname>pg_seclabel</structname> diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml index 3f61d7d..6cae6c3 100644 --- a/doc/src/sgml/ref/alter_table.sgml +++ b/doc/src/sgml/ref/alter_table.sgml @@ -68,6 +68,8 @@ ALTER TABLE [ IF EXISTS ] name NOT OF OWNER TO new_owner SET TABLESPACE new_tablespace + SET ROW LEVEL SECURITY (condition) + RESET ROW LEVEL SECURITY and table_constraint_using_index is: @@ -567,6 +569,29 @@ ALTER TABLE [ IF EXISTS ] name + SET ROW LEVEL SECURITY (condition) + + + This form set row-level security policy of the table. + Supplied condition performs + as if it is implicitly appended to the qualifiers of WHERE + clause, although mechanism guarantees to evaluate this condition earlier + than any other user given condition. + See also . + + + + + + RESET ROW LEVEL SECURITY + + + This form reset row-level security policy of the table, if exists. + + + + + RENAME @@ -806,6 +831,19 @@ ALTER TABLE [ IF EXISTS ] name + + condition + + + An expression that returns a value of type boolean. Expect for a case + when queries are executed with superuser privilege, only rows for which + this expression returns true will be fetched, updated or deleted. + This expression can reference columns of the relation being configured, + however, unavailable to include sub-query right now. + + + + diff --git a/doc/src/sgml/user-manag.sgml b/doc/src/sgml/user-manag.sgml index 177ac7a..c283e07 100644 --- a/doc/src/sgml/user-manag.sgml +++ b/doc/src/sgml/user-manag.sgml @@ -439,4 +439,127 @@ DROP ROLE name; + + Row-level Security + + PostgreSQL v9.3 or later provides + row-level security feature, like several commercial database + management system. It allows table owner to assign a particular + condition that performs as a security policy of the table; only + rows that satisfies the condition should be visible, except for + a case when superuser runs queries. + + + Row-level security policy can be set using + SET ROW LEVEL SECURITY command of + statement, as an expression + form that returns a value of type boolean. This expression can + contain references to columns of the relation, so it enables + to construct arbitrary rule to make access control decision + based on contents of each rows. + + + For example, the following customer table + has uname field to store user name, and + it assume we don't want to expose any properties of other + customers. + The following command set current_user = uname + as row-level security policy on the customer + table. + +postgres=> ALTER TABLE customer SET ROW LEVEL SECURITY (current_user = uname); +ALTER TABLE + + command shows how row-level + security policy works on the supplied query. + +postgres=> EXPLAIN(costs off) SELECT * FROM customer WHERE f_leak(upasswd); + QUERY PLAN +-------------------------------------------- + Subquery Scan on customer + Filter: f_leak(customer.upasswd) + -> Seq Scan on customer + Filter: ("current_user"() = uname) +(4 rows) + + This query execution plan means the preconfigured row-level + security policy is implicitly added, and scan plan on the + target relation being wrapped up with a sub-query. + It ensures user given qualifiers, including functions with + side effects, are never executed earlier than the row-level + security policy regardless of its cost, except for the cases + when these were fully leakproof. + This design helps to tackle the scenario described in + ; that introduces the order + to evaluate qualifiers is significant to keep confidentiality + of invisible rows. + + + + On the other hand, this design allows superusers to bypass + checks with row-level security. + It ensures pg_dump can obtain + a complete set of database backup, and avoid to execute + Trojan horse trap, being injected as a row-level security + policy of user-defined table, with privileges of superuser. + + + + In case of queries on inherited tables, row-level security + policy of the parent relation is not applied to child + relations. Scope of the row-level security policy is limited + to the relation on which it is set. + +postgres=> EXPLAIN(costs off) SELECT * FROM t1 WHERE f_leak(y); + QUERY PLAN +------------------------------------------- + Result + -> Append + -> Subquery Scan on t1 + Filter: f_leak(t1.y) + -> Seq Scan on t1 + Filter: ((x % 2) = 0) + -> Seq Scan on t2 + Filter: f_leak(y) + -> Subquery Scan on t3 + Filter: f_leak(t3.y) + -> Seq Scan on t3 + Filter: ((x % 2) = 1) +(12 rows) + + In the above example, t1 has inherited + child table t2 and t3, + and row-level security policy is set on only t1, + and t3, not t2. + + The row-level security policy of t1, + x must be even-number, is appended only + t1, neither t2 nor + t3. On the contrary, t3 + has different row-level security policy; x + must be odd-number. + + + + Right now, row-level security feature has several limitation, + although these shall be improved in the future version. + + Row-level security policy is not applied to + UPDATE or DELETE + commands in this revision, even though it should be + supported soon. + + Row-level security policy is not applied to rows to be inserted + and newer revision of updated rows, thus, it requires to + define before-row-insert or before-row-update trigger to check + whether the row's contents satisfies the policy individually. + + Although it is not a specific matter in row-level security, + and + allows to switch current user identifier during execution + of the query. Thus, it may cause unpredicated behavior + if and when current_user is used as + a part of row-level security policy. + + diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 49def6a..1b88a96 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -35,6 +35,7 @@ #include "executor/spi.h" #include "libpq/be-fsstubs.h" #include "miscadmin.h" +#include "optimizer/rowlevelsec.h" #include "pgstat.h" #include "replication/walsender.h" #include "replication/syncrep.h" @@ -142,6 +143,7 @@ typedef struct TransactionStateData int maxChildXids; /* allocated size of childXids[] */ Oid prevUser; /* previous CurrentUserId setting */ int prevSecContext; /* previous SecurityRestrictionContext */ + RowLevelSecMode prevRowLevelSecMode; /* previous RLS-mode setting */ bool prevXactReadOnly; /* entry-time xact r/o state */ bool startedInRecovery; /* did we start in recovery? */ struct TransactionStateData *parent; /* back link to parent */ @@ -171,6 +173,7 @@ static TransactionStateData TopTransactionStateData = { 0, /* allocated size of childXids[] */ InvalidOid, /* previous CurrentUserId setting */ 0, /* previous SecurityRestrictionContext */ + RowLevelSecModeEnabled, /* previous RowLevelSecMode setting */ false, /* entry-time xact r/o state */ false, /* startedInRecovery */ NULL /* link to parent state block */ @@ -1762,6 +1765,8 @@ StartTransaction(void) /* SecurityRestrictionContext should never be set outside a transaction */ Assert(s->prevSecContext == 0); + s->prevRowLevelSecMode = getRowLevelSecurityMode(); + /* * initialize other subsystems for new transaction */ @@ -2293,6 +2298,9 @@ AbortTransaction(void) */ SetUserIdAndSecContext(s->prevUser, s->prevSecContext); + /* Also, mode setting of row-level security should be restored. */ + setRowLevelSecurityMode(s->prevRowLevelSecMode); + /* * do abort processing */ @@ -4186,6 +4194,9 @@ AbortSubTransaction(void) */ SetUserIdAndSecContext(s->prevUser, s->prevSecContext); + /* Reset row-level security mode */ + setRowLevelSecurityMode(s->prevRowLevelSecMode); + /* * We can skip all this stuff if the subxact failed before creating a * ResourceOwner... @@ -4325,6 +4336,7 @@ PushTransaction(void) s->state = TRANS_DEFAULT; s->blockState = TBLOCK_SUBBEGIN; GetUserIdAndSecContext(&s->prevUser, &s->prevSecContext); + s->prevRowLevelSecMode = getRowLevelSecurityMode(); s->prevXactReadOnly = XactReadOnly; CurrentTransactionState = s; diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile index 62fc9b0..de840b2 100644 --- a/src/backend/catalog/Makefile +++ b/src/backend/catalog/Makefile @@ -14,7 +14,7 @@ OBJS = catalog.o dependency.o heap.o index.o indexing.o namespace.o aclchk.o \ objectaddress.o pg_aggregate.o pg_collation.o pg_constraint.o pg_conversion.o \ pg_depend.o pg_enum.o pg_inherits.o pg_largeobject.o pg_namespace.o \ pg_operator.o pg_proc.o pg_range.o pg_db_role_setting.o pg_shdepend.o \ - pg_type.o storage.o toasting.o + pg_rowlevelsec.o pg_type.o storage.o toasting.o BKIFILES = postgres.bki postgres.description postgres.shdescription @@ -38,7 +38,7 @@ POSTGRES_BKI_SRCS = $(addprefix $(top_srcdir)/src/include/catalog/,\ pg_ts_config.h pg_ts_config_map.h pg_ts_dict.h \ pg_ts_parser.h pg_ts_template.h pg_extension.h \ pg_foreign_data_wrapper.h pg_foreign_server.h pg_user_mapping.h \ - pg_foreign_table.h \ + pg_foreign_table.h pg_rowlevelsec.h \ pg_default_acl.h pg_seclabel.h pg_shseclabel.h pg_collation.h pg_range.h \ toasting.h indexing.h \ ) diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index 98ce598..d18584b 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -45,6 +45,7 @@ #include "catalog/pg_opfamily.h" #include "catalog/pg_proc.h" #include "catalog/pg_rewrite.h" +#include "catalog/pg_rowlevelsec.h" #include "catalog/pg_tablespace.h" #include "catalog/pg_trigger.h" #include "catalog/pg_ts_config.h" @@ -1221,6 +1222,10 @@ doDeletion(const ObjectAddress *object, int flags) RemoveExtensionById(object->objectId); break; + case OCLASS_ROWLEVELSEC: + RemoveRowLevelSecurityById(object->objectId); + break; + default: elog(ERROR, "unrecognized object class: %u", object->classId); @@ -2269,6 +2274,9 @@ getObjectClass(const ObjectAddress *object) case ExtensionRelationId: return OCLASS_EXTENSION; + + case RowLevelSecurityRelationId: + return OCLASS_ROWLEVELSEC; } /* shouldn't get here */ @@ -2903,6 +2911,19 @@ getObjectDescription(const ObjectAddress *object) break; } + case OCLASS_ROWLEVELSEC: + { + char *relname; + + relname = get_rel_name(object->objectId); + if (!relname) + elog(ERROR, "cache lookup failed for relation %u", + object->objectId); + appendStringInfo(&buffer, + _("row-level security of %s"), relname); + break; + } + default: appendStringInfo(&buffer, "unrecognized object %u %u %d", object->classId, diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c index c91df90..0d806de 100644 --- a/src/backend/catalog/heap.c +++ b/src/backend/catalog/heap.c @@ -775,6 +775,7 @@ InsertPgClassTuple(Relation pg_class_desc, values[Anum_pg_class_relhaspkey - 1] = BoolGetDatum(rd_rel->relhaspkey); values[Anum_pg_class_relhasrules - 1] = BoolGetDatum(rd_rel->relhasrules); values[Anum_pg_class_relhastriggers - 1] = BoolGetDatum(rd_rel->relhastriggers); + values[Anum_pg_class_relhasrowlevelsec - 1] = BoolGetDatum(rd_rel->relhasrowlevelsec); values[Anum_pg_class_relhassubclass - 1] = BoolGetDatum(rd_rel->relhassubclass); values[Anum_pg_class_relfrozenxid - 1] = TransactionIdGetDatum(rd_rel->relfrozenxid); if (relacl != (Datum) 0) diff --git a/src/backend/catalog/pg_rowlevelsec.c b/src/backend/catalog/pg_rowlevelsec.c new file mode 100644 index 0000000..bd89c48 --- /dev/null +++ b/src/backend/catalog/pg_rowlevelsec.c @@ -0,0 +1,250 @@ +/* ------------------------------------------------------------------------- + * + * pg_rowlevelsec.c + * routines to support manipulation of the pg_rowlevelsec relation + * + * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * ------------------------------------------------------------------------- + */ +#include "postgres.h" +#include "access/genam.h" +#include "access/heapam.h" +#include "catalog/dependency.h" +#include "catalog/indexing.h" +#include "catalog/pg_class.h" +#include "catalog/pg_rowlevelsec.h" +#include "catalog/pg_type.h" +#include "nodes/nodeFuncs.h" +#include "optimizer/clauses.h" +#include "parser/parse_clause.h" +#include "parser/parse_node.h" +#include "parser/parse_relation.h" +#include "utils/builtins.h" +#include "utils/fmgroids.h" +#include "utils/rel.h" +#include "utils/syscache.h" +#include "utils/tqual.h" + +void +RelationBuildRowLevelSecurity(Relation relation) +{ + Relation rlsrel; + ScanKeyData skey; + SysScanDesc sscan; + HeapTuple tuple; + + rlsrel = heap_open(RowLevelSecurityRelationId, AccessShareLock); + + ScanKeyInit(&skey, + Anum_pg_rowlevelsec_rlsrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationGetRelid(relation))); + sscan = systable_beginscan(rlsrel, RowLevelSecurityIndexId, true, + SnapshotNow, 1, &skey); + + tuple = systable_getnext(sscan); + if (HeapTupleIsValid(tuple)) + { + RowLevelSecDesc *rlsdesc; + MemoryContext rlscxt; + MemoryContext oldcxt; + Datum datum; + bool isnull; + char *temp; + + /* + * Make the private memory context to store RowLevelSecDesc that + * includes expression tree also. + */ + rlscxt = AllocSetContextCreate(CacheMemoryContext, + RelationGetRelationName(relation), + ALLOCSET_SMALL_MINSIZE, + ALLOCSET_SMALL_INITSIZE, + ALLOCSET_SMALL_MAXSIZE); + PG_TRY(); + { + datum = heap_getattr(tuple, Anum_pg_rowlevelsec_rlsqual, + RelationGetDescr(rlsrel), &isnull); + Assert(!isnull); + temp = TextDatumGetCString(datum); + + oldcxt = MemoryContextSwitchTo(rlscxt); + + rlsdesc = palloc0(sizeof(RowLevelSecDesc)); + rlsdesc->rlscxt = rlscxt; + rlsdesc->rlsqual = (Expr *) stringToNode(temp); + Assert(exprType((Node *)rlsdesc->rlsqual) == BOOLOID); + + rlsdesc->rlshassublinks + = contain_subplans((Node *)rlsdesc->rlsqual); + + MemoryContextSwitchTo(oldcxt); + + pfree(temp); + } + PG_CATCH(); + { + MemoryContextDelete(rlscxt); + PG_RE_THROW(); + } + PG_END_TRY(); + + relation->rlsdesc = rlsdesc; + } + else + { + relation->rlsdesc = NULL; + } + systable_endscan(sscan); + heap_close(rlsrel, AccessShareLock); +} + +void +SetRowLevelSecurity(Relation relation, Node *clause) +{ + Oid relationId = RelationGetRelid(relation); + ParseState *pstate; + RangeTblEntry *rte; + Node *qual; + Relation rlsrel; + ScanKeyData skey; + SysScanDesc sscan; + HeapTuple oldtup; + HeapTuple newtup; + Datum values[Natts_pg_rowlevelsec]; + bool isnull[Natts_pg_rowlevelsec]; + bool replaces[Natts_pg_rowlevelsec]; + ObjectAddress target; + ObjectAddress myself; + + /* Parse the supplied clause */ + pstate = make_parsestate(NULL); + + rte = addRangeTableEntryForRelation(pstate, relation, + NULL, false, false); + addRTEtoQuery(pstate, rte, false, true, true); + + qual = transformWhereClause(pstate, copyObject(clause), + "ROW LEVEL SECURITY"); + /* No aggregate function support */ + if (pstate->p_hasAggs) + ereport(ERROR, + (errcode(ERRCODE_GROUPING_ERROR), + errmsg("cannot use aggregate in row-level security"))); + /* No window function support */ + if (pstate->p_hasWindowFuncs) + ereport(ERROR, + (errcode(ERRCODE_WINDOWING_ERROR), + errmsg("cannot use window function in row-level security"))); + + /* zero-clear */ + memset(values, 0, sizeof(values)); + memset(replaces, 0, sizeof(replaces)); + memset(isnull, 0, sizeof(isnull)); + + /* Update or Indert an entry to pg_rowlevelsec catalog */ + rlsrel = heap_open(RowLevelSecurityRelationId, RowExclusiveLock); + + ScanKeyInit(&skey, + Anum_pg_rowlevelsec_rlsrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationGetRelid(relation))); + sscan = systable_beginscan(rlsrel, RowLevelSecurityIndexId, true, + SnapshotNow, 1, &skey); + oldtup = systable_getnext(sscan); + if (HeapTupleIsValid(oldtup)) + { + replaces[Anum_pg_rowlevelsec_rlsqual - 1] = true; + values[Anum_pg_rowlevelsec_rlsqual - 1] + = CStringGetTextDatum(nodeToString(qual)); + + newtup = heap_modify_tuple(oldtup, + RelationGetDescr(rlsrel), + values, isnull, replaces); + simple_heap_update(rlsrel, &newtup->t_self, newtup); + + deleteDependencyRecordsFor(RowLevelSecurityRelationId, + relationId, false); + } + else + { + values[Anum_pg_rowlevelsec_rlsrelid - 1] + = ObjectIdGetDatum(relationId); + values[Anum_pg_rowlevelsec_rlsqual - 1] + = CStringGetTextDatum(nodeToString(qual)); + newtup = heap_form_tuple(RelationGetDescr(rlsrel), + values, isnull); + simple_heap_insert(rlsrel, newtup); + } + CatalogUpdateIndexes(rlsrel, newtup); + + heap_freetuple(newtup); + + /* records dependencies of RLS-policy and relation/columns */ + target.classId = RelationRelationId; + target.objectId = relationId; + target.objectSubId = 0; + + myself.classId = RowLevelSecurityRelationId; + myself.objectId = relationId; + myself.objectSubId = 0; + + recordDependencyOn(&myself, &target, DEPENDENCY_AUTO); + + recordDependencyOnExpr(&myself, qual, pstate->p_rtable, + DEPENDENCY_NORMAL); + free_parsestate(pstate); + + systable_endscan(sscan); + heap_close(rlsrel, RowExclusiveLock); +} + +void +ResetRowLevelSecurity(Relation relation) +{ + if (relation->rlsdesc) + { + ObjectAddress address; + + address.classId = RowLevelSecurityRelationId; + address.objectId = RelationGetRelid(relation); + address.objectSubId = 0; + + performDeletion(&address, DROP_RESTRICT, 0); + } + else + { + /* Nothing to do here */ + elog(INFO, "relation %s has no row-level security policy, skipped", + RelationGetRelationName(relation)); + } +} + +/* + * Guts of Row-level security policy deletion. + */ +void +RemoveRowLevelSecurityById(Oid relationId) +{ + Relation rlsrel; + ScanKeyData skey; + SysScanDesc sscan; + HeapTuple tuple; + + rlsrel = heap_open(RowLevelSecurityRelationId, RowExclusiveLock); + + ScanKeyInit(&skey, + Anum_pg_rowlevelsec_rlsrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(relationId)); + sscan = systable_beginscan(rlsrel, RowLevelSecurityIndexId, true, + SnapshotNow, 1, &skey); + while (HeapTupleIsValid(tuple = systable_getnext(sscan))) + { + simple_heap_delete(rlsrel, &tuple->t_self); + } + systable_endscan(sscan); + heap_close(rlsrel, RowExclusiveLock); +} diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index d69809a..6aae6f8 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -35,6 +35,7 @@ #include "catalog/pg_inherits_fn.h" #include "catalog/pg_namespace.h" #include "catalog/pg_opclass.h" +#include "catalog/pg_rowlevelsec.h" #include "catalog/pg_tablespace.h" #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" @@ -384,6 +385,7 @@ static void ATExecDropInherit(Relation rel, RangeVar *parent, LOCKMODE lockmode) static void drop_parent_dependency(Oid relid, Oid refclassid, Oid refobjid); static void ATExecAddOf(Relation rel, const TypeName *ofTypename, LOCKMODE lockmode); static void ATExecDropOf(Relation rel, LOCKMODE lockmode); +static void ATExecSetRowLevelSecurity(Relation relation, Node *clause); static void ATExecGenericOptions(Relation rel, List *options); static void copy_relation_data(SMgrRelation rel, SMgrRelation dst, @@ -2746,6 +2748,8 @@ AlterTableGetLockLevel(List *cmds) case AT_SetTableSpace: /* must rewrite heap */ case AT_DropNotNull: /* may change some SQL plans */ case AT_SetNotNull: + case AT_SetRowLevelSecurity: + case AT_ResetRowLevelSecurity: case AT_GenericOptions: case AT_AlterColumnGenericOptions: cmd_lockmode = AccessExclusiveLock; @@ -3108,6 +3112,8 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd, case AT_DropInherit: /* NO INHERIT */ case AT_AddOf: /* OF */ case AT_DropOf: /* NOT OF */ + case AT_SetRowLevelSecurity: + case AT_ResetRowLevelSecurity: ATSimplePermissions(rel, ATT_TABLE); /* These commands never recurse */ /* No command-specific prep needed */ @@ -3383,6 +3389,12 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab, Relation rel, case AT_DropOf: ATExecDropOf(rel, lockmode); break; + case AT_SetRowLevelSecurity: + ATExecSetRowLevelSecurity(rel, (Node *) cmd->def); + break; + case AT_ResetRowLevelSecurity: + ATExecSetRowLevelSecurity(rel, NULL); + break; case AT_GenericOptions: ATExecGenericOptions(rel, (List *) cmd->def); break; @@ -7558,6 +7570,22 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, Assert(defaultexpr); break; + case OCLASS_ROWLEVELSEC: + /* + * A row-level security policy can depend on a column in case + * when the policy clause references a particular column. + * Due to same reason why TRIGGER ... WHEN does not support + * to change column's type being referenced in clause, row- + * level security policy also does not support it. + */ + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type of a column used in a row-level security policy"), + errdetail("%s depends on column \"%s\"", + getObjectDescription(&foundObject), + colName))); + break; + case OCLASS_PROC: case OCLASS_TYPE: case OCLASS_CAST: @@ -9656,6 +9684,42 @@ ATExecDropOf(Relation rel, LOCKMODE lockmode) } /* + * ALTER TABLE SET ROW LEVEL SECURITY (...) OR + * RESET ROW LEVEL SECURITY + */ +static void +ATExecSetRowLevelSecurity(Relation relation, Node *clause) +{ + Oid relid = RelationGetRelid(relation); + Relation class_rel; + HeapTuple tuple; + Form_pg_class class_form; + + if (clause != NULL) + SetRowLevelSecurity(relation, clause); + else + ResetRowLevelSecurity(relation); + + class_rel = heap_open(RelationRelationId, RowExclusiveLock); + + tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for relation %u", relid); + + class_form = (Form_pg_class) GETSTRUCT(tuple); + if (clause != NULL) + class_form->relhasrowlevelsec = true; + else + class_form->relhasrowlevelsec = false; + + simple_heap_update(class_rel, &tuple->t_self, tuple); + CatalogUpdateIndexes(class_rel, tuple); + + heap_close(class_rel, RowExclusiveLock); + heap_freetuple(tuple); +} + +/* * ALTER FOREIGN TABLE OPTIONS (...) */ static void diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index df76341..dabcfc6 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -32,6 +32,7 @@ #include "optimizer/planmain.h" #include "optimizer/planner.h" #include "optimizer/prep.h" +#include "optimizer/rowlevelsec.h" #include "optimizer/subselect.h" #include "optimizer/tlist.h" #include "parser/analyze.h" @@ -163,6 +164,13 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams) glob->lastPHId = 0; glob->lastRowMarkId = 0; glob->transientPlan = false; + glob->planUserId = InvalidOid; + /* + * XXX - a valid user-id shall be set on planUserId later, if constructed + * plan assumes being executed under privilege of a particular user-id. + * Elsewhere, keep InvalidOid; that means the constructed plan is portable + * for any users. + */ /* Determine what fraction of the plan is likely to be scanned */ if (cursorOptions & CURSOR_OPT_FAST_PLAN) @@ -240,6 +248,7 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams) result->relationOids = glob->relationOids; result->invalItems = glob->invalItems; result->nParamExec = list_length(glob->paramlist); + result->planUserId = glob->planUserId; return result; } @@ -314,6 +323,14 @@ subquery_planner(PlannerGlobal *glob, Query *parse, SS_process_ctes(root); /* + * Apply row-level security policy of appeared tables, if configured. + * It must be applied prior to preprocess_rowmarks(). + * + * + */ + applyRowLevelSecurity(root); + + /* * Look for ANY and EXISTS SubLinks in WHERE and JOIN/ON clauses, and try * to transform them into joins. Note that this step does not descend * into subqueries; if we pull up any subqueries below, their SubLinks are diff --git a/src/backend/optimizer/util/Makefile b/src/backend/optimizer/util/Makefile index 3b2d16b..3430689 100644 --- a/src/backend/optimizer/util/Makefile +++ b/src/backend/optimizer/util/Makefile @@ -13,6 +13,6 @@ top_builddir = ../../../.. include $(top_builddir)/src/Makefile.global OBJS = clauses.o joininfo.o pathnode.o placeholder.o plancat.o predtest.o \ - relnode.o restrictinfo.o tlist.o var.o + relnode.o restrictinfo.o tlist.o var.o rowlevelsec.o include $(top_srcdir)/src/backend/common.mk diff --git a/src/backend/optimizer/util/rowlevelsec.c b/src/backend/optimizer/util/rowlevelsec.c new file mode 100644 index 0000000..70a1336 --- /dev/null +++ b/src/backend/optimizer/util/rowlevelsec.c @@ -0,0 +1,677 @@ +/* + * optimizer/util/rowlvsec.c + * Row-level security support routines + * + * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + */ +#include "postgres.h" + +#include "access/heapam.h" +#include "access/sysattr.h" +#include "catalog/pg_class.h" +#include "catalog/pg_inherits_fn.h" +#include "catalog/pg_rowlevelsec.h" +#include "catalog/pg_type.h" +#include "miscadmin.h" +#include "nodes/makefuncs.h" +#include "nodes/nodeFuncs.h" +#include "nodes/plannodes.h" +#include "optimizer/clauses.h" +#include "optimizer/prep.h" +#include "optimizer/rowlevelsec.h" +#include "parser/parsetree.h" +#include "utils/lsyscache.h" +#include "utils/rel.h" +#include "utils/syscache.h" + +/* flags to pull row-level security policy */ +#define RLS_FLAG_HAS_SUBLINKS 0x0001 + +/* hook to allow extensions to apply their own security policy */ +rowlevel_security_hook_type rowlevel_security_hook = NULL; + +/* current performing mode of row-level security policy */ +static RowLevelSecMode rowlevel_security_mode = RowLevelSecModeEnabled; + +/* + * getRowLevelSecurityMode / setRowLevelSecurityMode + * + * These functions allow to get or set current performing mode of row- + * level security feature. It enables to disable this feature temporarily + * for some cases in which row-level security prevent correct behavior + * such as foreign-key checks to prohibit update of PKs being referenced + * by others. + * The caller must ensure the saved previous mode shall be restored, but + * no need to care about cases when an error would be raised. + */ +RowLevelSecMode +getRowLevelSecurityMode(void) +{ + return rowlevel_security_mode; +} + +void +setRowLevelSecurityMode(RowLevelSecMode new_mode) +{ + rowlevel_security_mode = new_mode; +} + +/* + * pull_rowlevel_security_policy + * + * This routine tries to pull expression node of row-level security policy + * on the target relation, and its children if configured. + * If one or more relation has a security policy at least, this function + * returns true, or false elsewhere. + */ +static bool +pull_rowlevel_security_policy(PlannerInfo *root, + RangeTblEntry *rte, + Index rtindex, + List **rls_relids, + List **rls_quals, + List **rls_flags) +{ + Relation rel; + LOCKMODE lockmode; + List *relid_list = NIL; + List *qual_list = NIL; + List *flag_list = NIL; + ListCell *cell; + bool result = false; + + Assert(rte->rtekind == RTE_RELATION && OidIsValid(rte->relid)); + + if (!rte->inh) + { + lockmode = NoLock; + relid_list = list_make1_oid(rte->relid); + } + else + { + /* + * In case when the target relation may have inheritances, + * we need to suggest an appropriate lock mode because it + * is the first time to reference these tables in a series + * of processes. For more details, see the comments in + * expand_inherited_tables. + * Also note that it does not guarantee the locks on child + * tables being already acquired at expand_inherited_tables, + * because row-level security routines can be bypassed if + * RowLevelSecModeDisabled. + */ + if (rtindex == root->parse->resultRelation) + lockmode = RowExclusiveLock; + else + { + foreach (cell, root->parse->rowMarks) + { + Assert(IsA(lfirst(cell), RowMarkClause)); + if (((RowMarkClause *) lfirst(cell))->rti == rtindex) + break; + } + if (cell) + lockmode = RowShareLock; + else + lockmode = AccessShareLock; + } + relid_list = find_all_inheritors(rte->relid, lockmode, NULL); + } + + /* + * Try to fetch row-level security policy of the target relation + * or its children. + */ + foreach (cell, relid_list) + { + Expr *qual = NULL; + int flags = 0; + + rel = heap_open(lfirst_oid(cell), lockmode); + + /* + * Pull out row-level security policy configured with built-in + * features, if unprivileged users. Please note that superuser + * can bypass it. + */ + if (rel->rlsdesc && !superuser()) + { + RowLevelSecDesc *rlsdesc = rel->rlsdesc; + + qual = copyObject(rlsdesc->rlsqual); + if (rlsdesc->rlshassublinks) + flags |= RLS_FLAG_HAS_SUBLINKS; + } + + /* + * Also, ask extensions whether they want to apply their own + * row-level security policy. If both built-in and extension + * has their own policy, it shall be merged. + */ + if (rowlevel_security_hook) + { + List *qual_list; + + qual_list = (*rowlevel_security_hook)(root, rel); + if (qual_list != NIL) + { + if ((flags & RLS_FLAG_HAS_SUBLINKS) == 0 && + contain_subplans((Node *)qual_list)) + flags |= RLS_FLAG_HAS_SUBLINKS; + + if (qual != NULL) + qual_list = lappend(qual_list, qual); + + if (list_length(qual_list) == 1) + qual = (Expr *)list_head(qual_list); + else + qual = makeBoolExpr(AND_EXPR, qual_list, -1); + } + } + + qual_list = lappend(qual_list, qual); + if (qual) + result = true; + flag_list = lappend_int(flag_list, flags); + + heap_close(rel, NoLock); /* close the relation, but keep locks */ + } + + /* + * Inform the caller list of relation Oid, qualifier of row-level + * security policy and its flag, if one or more target relations + * have its row-level security policy. Elsewhere, release it. + */ + if (result) + { + *rls_relids = relid_list; + *rls_quals = qual_list; + *rls_flags = flag_list; + } + else + { + list_free(relid_list); + list_free(qual_list); + list_free(flag_list); + } + return result; +} + +/* + * fixup_varattnos + * + * It fixes up varattno of Var node that referenced the relation with + * RLS policy, thus replaced to a sub-query. Here is no guarantee the + * varattno matches with TargetEntry's resno of the sub-query, so needs + * to adjust them. + */ +typedef struct { + PlannerInfo *root; + int varlevelsup; +} fixup_var_context; + +static bool +fixup_var_references_walker(Node *node, fixup_var_context *context) +{ + if (node == NULL) + return false; + + if (IsA(node, Var)) + { + Var *var = (Var *)node; + RangeTblEntry *rte; + + /* + * Does this Var node reference the Query node currently we focused + * on. If not, we simply ignore it. + */ + if (var->varlevelsup != context->varlevelsup) + return false; + + rte = rt_fetch(var->varno, context->root->parse->rtable); + if (!rte) + elog(ERROR, "invalid varno %d", var->varno); + + if (rte->rtekind == RTE_SUBQUERY && + rte->subquery->querySource == QSRC_ROW_LEVEL_SECURITY) + { + List *targetList = rte->subquery->targetList; + ListCell *cell; + + foreach (cell, targetList) + { + TargetEntry *subtle = lfirst(cell); + + if ((IsA(subtle->expr, ConvertRowtypeExpr) && + var->varattno == InvalidAttrNumber) || + (IsA(subtle->expr, Var) && + var->varattno == ((Var *)(subtle->expr))->varattno)) + { + var->varattno = subtle->resno; + return false; + } + } + elog(ERROR, "invalid varattno %d", var->varattno); + } + return false; + } + else if (IsA(node, Query)) + { + bool result; + + context->varlevelsup++; + result = query_tree_walker((Query *) node, + fixup_var_references_walker, + (void *) context, 0); + context->varlevelsup--; + + return result; + } + return expression_tree_walker(node, + fixup_var_references_walker, + (void *) context); +} + +static void +fixup_varattnos(PlannerInfo *root) +{ + fixup_var_context context; + + /* + * Fixup Var->varattno that references the sub-queries originated from + * regular relations with RLS policy. + */ + context.root = root; + context.varlevelsup = 0; + + query_tree_walker(root->parse, + fixup_var_references_walker, + (void *) &context, 0); +} + +/* + * make_pseudo_column + * + * make a TargetEntry object which references a particular column of + * the underlying table. + */ +static TargetEntry * +make_pseudo_column(Oid relid_head, Oid relid, AttrNumber attnum) +{ + Form_pg_attribute attform; + HeapTuple tuple; + Var *var; + char *resname; + + if (attnum == InvalidAttrNumber) + { + ConvertRowtypeExpr *r = makeNode(ConvertRowtypeExpr); + + r->arg = (Expr *) makeVar((Index) 1, + InvalidAttrNumber, + get_rel_type_id(relid), + -1, + InvalidOid, + 0); + r->resulttype = get_rel_type_id(relid_head); + r->convertformat = COERCE_IMPLICIT_CAST; + r->location = -1; + + return makeTargetEntry((Expr *) r, -1, get_rel_name(relid), false); + } + + tuple = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(relid), + Int16GetDatum(attnum)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + attnum, relid); + attform = (Form_pg_attribute) GETSTRUCT(tuple); + + var = makeVar((Index) 1, + attform->attnum, + attform->atttypid, + attform->atttypmod, + InvalidOid, + 0); + resname = pstrdup(NameStr(attform->attname)); + + ReleaseSysCache(tuple); + + return makeTargetEntry((Expr *)var, -1, resname, false); +} + +/* + * make_pseudo_subquery + * + * This routine makes a sub-query that references the target relation + * with given row-level security policy. This sub-query shall have + * security_barrier attribute to prevent unexpected push-down. + */ +static Query * +make_pseudo_subquery(Oid relid_head, + Oid relid, + Node *qual, + int flags, + List *targetList, + RangeTblEntry *rte, + RowMarkClause *rclause) +{ + Query *subqry; + RangeTblEntry *subrte; + RangeTblRef *subrtr; + List *colnameList = NIL; + ListCell *cell; + + subqry = makeNode(Query); + subqry->commandType = CMD_SELECT; + subqry->querySource = QSRC_ROW_LEVEL_SECURITY; + + subrte = makeNode(RangeTblEntry); + subrte->rtekind = RTE_RELATION; + subrte->relid = relid; + subrte->relkind = get_rel_relkind(relid); + subrte->inFromCl = true; + subrte->requiredPerms = rte->requiredPerms; + subrte->selectedCols = NULL; + subrte->modifiedCols = NULL; + + foreach (cell, targetList) + { + TargetEntry *oldtle = lfirst(cell); + TargetEntry *subtle; + AttrNumber attnum; + + if (IsA(oldtle->expr, ConvertRowtypeExpr)) + attnum = InvalidAttrNumber; + else + { + Assert(IsA(oldtle->expr, Var)); + + attnum = get_attnum(relid, oldtle->resname); + if (attnum == InvalidAttrNumber) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("column \"%s\" of relation \"%s\" does not exist", + oldtle->resname, get_rel_name(relid)))); + } + subtle = make_pseudo_column(relid_head, relid, attnum); + subtle->resno = oldtle->resno; + subqry->targetList = lappend(subqry->targetList, subtle); + + colnameList = lappend(colnameList, + makeString(pstrdup(subtle->resname))); + attnum -= FirstLowInvalidHeapAttributeNumber; + subrte->selectedCols = bms_add_member(rte->selectedCols, attnum); + } + subrte->eref = makeAlias(get_rel_name(relid), colnameList); + + if (flags & RLS_FLAG_HAS_SUBLINKS) + subqry->hasSubLinks = true; + + subqry->rtable = list_make1(subrte); + + subrtr = makeNode(RangeTblRef); + subrtr->rtindex = 1; + subqry->jointree = makeFromExpr(list_make1(subrtr), qual); + + if (rclause) + { + RowMarkClause *rclause_sub; + + rclause_sub = copyObject(rclause); + rclause_sub->rti = 1; + + subqry->rowMarks = list_make1(rclause); + subqry->hasForUpdate = true; + } + return subqry; +} + +/* + * expand_rtentry_with_policy + * + * This routine expand reference to the given RangeTblEntry by a sub-query + * which simply references the target relation with the qualifier of row- + * level security policy. + */ +static void +expand_rtentry_with_policy(PlannerInfo *root, + RangeTblEntry *rte, + Index rtindex, + List *rls_relids, + List *rls_quals, + List *rls_flags) +{ + Query *parse = root->parse; + Oid relid_head; + Query *subqry; + RangeTblEntry *subrte; + TargetEntry *subtle; + List *targetList; + List *colnameList; + Bitmapset *attr_used; + AttrNumber attnum; + RowMarkClause *rclause; + ListCell *cell1; + ListCell *cell2; + ListCell *cell3; + ListCell *lc; + + Assert(rte->rtekind == RTE_RELATION && OidIsValid(rte->relid)); + Assert(list_length(rls_relids) == list_length(rls_quals)); + Assert(list_length(rls_relids) == list_length(rls_flags)); + Assert(list_length(rls_relids) > 0); + + /* + * Construct a target-entry list + */ + targetList = NIL; + colnameList = NIL; + relid_head = linitial_oid(rls_relids); + attr_used = bms_union(rte->selectedCols, + rte->modifiedCols); + while ((attnum = bms_first_member(attr_used)) >= 0) + { + attnum += FirstLowInvalidHeapAttributeNumber; + + subtle = make_pseudo_column(relid_head, relid_head, attnum); + subtle->resno = list_length(targetList) + 1; + + targetList = lappend(targetList, subtle); + colnameList = lappend(colnameList, + makeString(pstrdup(subtle->resname))); + } + bms_free(attr_used); + + /* + * Push-down row-level lock of the target relation, since sub-query + * does not support FOR SHARE/FOR UPDATE locks being assigned. + */ + rclause = NULL; + foreach (lc, parse->rowMarks) + { + if (((RowMarkClause *) lfirst(lc))->rti == rtindex) + { + rclause = lfirst(lc); + parse->rowMarks = list_delete(parse->rowMarks, rclause); + break; + } + } + + /* + * Construct sub-query structures + */ + forthree (cell1, rls_relids, cell2, rls_quals, cell3, rls_flags) + { + Oid relid = lfirst_oid(cell1); + Node *qual = lfirst(cell2); + int flags = lfirst_int(cell3); + + subqry = make_pseudo_subquery(relid_head, relid, qual, flags, + targetList, rte, rclause); + if (cell1 == list_head(rls_relids)) + { + Assert(rte->relid == relid); + Assert(rte->inh == true || list_length(rls_relids) == 1); + + rte->relid = InvalidOid; + rte->rtekind = RTE_SUBQUERY; + rte->subquery = subqry; + rte->security_barrier = true; + + /* no permission checks are needed to subquery itself */ + rte->requiredPerms = 0; + rte->checkAsUser = InvalidOid; + rte->selectedCols = NULL; + rte->modifiedCols = NULL; + + rte->alias = NULL; + rte->eref = makeAlias(get_rel_name(relid), + copyObject(colnameList)); + if (list_length(rls_relids) == 1) + rte->inh = false; + } + + /* + * RTE's for child relations and AppendRelInfo in case when + * the target relation has its children. + */ + if (list_length(rls_relids) > 1) + { + AppendRelInfo *apinfo; + AttrNumber child_rtindex; + ListCell *l; + + /* + * XXX - Set up RangeTblEntry for the child relation. + * Note that it does not need to have security_barrier + * attribute, if no row-level security policy is + * configured on. + */ + subrte = makeNode(RangeTblEntry); + subrte->rtekind = RTE_SUBQUERY; + subrte->subquery = subqry; + if (qual) + subrte->security_barrier = true; + subrte->alias = NULL; + subrte->eref = makeAlias(get_rel_name(relid), + copyObject(colnameList)); + parse->rtable = lappend(parse->rtable, subrte); + child_rtindex = list_length(parse->rtable); + + /* + * reference to inherited children performs as if simple + * UNION ALL operation, so add AppendRelInfo here. + */ + apinfo = makeNode(AppendRelInfo); + apinfo->parent_relid = rtindex; + apinfo->child_relid = child_rtindex; + foreach (l, targetList) + { + Var *trans_var + = makeVarFromTargetEntry(child_rtindex, lfirst(l)); + apinfo->translated_vars + = lappend(apinfo->translated_vars, trans_var); + } + root->append_rel_list = lappend(root->append_rel_list, apinfo); + } + } +} + +/* + * applyRowLevelSecurity + * + * It tries to apply row-level security policy of the relation. + * If and when a particular policy is configured on the referenced + * relation, it shall be replaced by a sub-query with security-barrier flag; + * that references the relation with row-level security policy. + * In the result, all users can see is rows of the relation that satisfies + * the condition supplied as security policy. + */ +void +applyRowLevelSecurity(PlannerInfo *root) +{ + Query *parse = root->parse; + ListCell *cell; + Index rtindex; + bool has_rowlevel_sec = false; + + /* mode checks */ + if (rowlevel_security_mode == RowLevelSecModeDisabled) + return; + + /* + * No need to apply row-level security on sub-query being originated + * from regular relation with RLS policy any more. + */ + if (parse->querySource == QSRC_ROW_LEVEL_SECURITY) + return; + + rtindex = 0; + foreach (cell, parse->rtable) + { + RangeTblEntry *rte = lfirst(cell); + List *rls_relids; + List *rls_quals; + List *rls_flags; + + rtindex++; + + if (rte->rtekind != RTE_RELATION) + continue; + + /* + * XXX - In this revision, we have no support for UPDATE / DELETE + * statement, so simply skip it. + */ + if (rtindex == parse->resultRelation) + continue; + + /* + * In case when a row-level security policy was configured on + * the table referenced by this RangeTblEntry or its children, + * it shall be rewritten to sub-query with this policy. + */ + if (pull_rowlevel_security_policy(root, rte, rtindex, + &rls_relids, &rls_quals, &rls_flags)) + { + expand_rtentry_with_policy(root, rte, rtindex, + rls_relids, rls_quals, rls_flags); + has_rowlevel_sec = true; + } + } + + /* + * Post case handling if one or more relation was replaced to sub-query. + */ + if (has_rowlevel_sec) + { + PlanInvalItem *pi_item; + + /* + * plan should be invalidated if and when userid was changed + * on the executor stage, from planner stage. + */ + Assert(!OidIsValid(root->glob->planUserId) || + root->glob->planUserId == GetUserId()); + root->glob->planUserId = GetUserId(); + + pi_item = makeNode(PlanInvalItem); + pi_item->cacheId = AUTHOID; + pi_item->hashValue + = GetSysCacheHashValue1(AUTHOID, + ObjectIdGetDatum(root->glob->planUserId)); + root->glob->invalItems = lappend(root->glob->invalItems, pi_item); + + /* + * Since the relation with RLS policy was replaced by a sub-query, + * thus resource number to reference a particular column can be + * also moditifed. If we applied RLS policy on one or more relations, + * varattno of Var node that has referenced the rewritten relation + * needs to be fixed up. + */ + fixup_varattnos(root); + } +} diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 7e6ceed..8c0a8a4 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -2058,6 +2058,22 @@ alter_table_cmd: n->def = (Node *)$2; $$ = (Node *)n; } + /* ALTER TABLE SET ROW LEVEL SECURITY (expression) */ + | SET ROW LEVEL SECURITY '(' a_expr ')' + { + AlterTableCmd *n = makeNode(AlterTableCmd); + n->subtype = AT_SetRowLevelSecurity; + n->def = (Node *) $6; + $$ = (Node *)n; + } + /* ALTER TABLE RESET ROW LEVEL SECURITY */ + | RESET ROW LEVEL SECURITY + { + AlterTableCmd *n = makeNode(AlterTableCmd); + n->subtype = AT_ResetRowLevelSecurity; + n->def = NULL; + $$ = (Node *)n; + } | alter_generic_options { AlterTableCmd *n = makeNode(AlterTableCmd); diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c index 983f631..c53cbfd 100644 --- a/src/backend/utils/adt/ri_triggers.c +++ b/src/backend/utils/adt/ri_triggers.c @@ -42,6 +42,7 @@ #include "parser/parse_coerce.h" #include "parser/parse_relation.h" #include "miscadmin.h" +#include "optimizer/rowlevelsec.h" #include "utils/builtins.h" #include "utils/fmgroids.h" #include "utils/guc.h" @@ -2998,6 +2999,7 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo, int spi_result; Oid save_userid; int save_sec_context; + RowLevelSecMode save_rls_mode; Datum vals[RI_MAX_NUMKEYS * 2]; char nulls[RI_MAX_NUMKEYS * 2]; @@ -3080,6 +3082,15 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo, SetUserIdAndSecContext(RelationGetForm(query_rel)->relowner, save_sec_context | SECURITY_LOCAL_USERID_CHANGE); + /* + * Disabled row-level security in case when foreign-key relation is + * queried to check existence of tupls that references the tuple to + * be modified on the primary-key side. + */ + save_rls_mode = getRowLevelSecurityMode(); + if (source_is_pk) + setRowLevelSecurityMode(RowLevelSecModeDisabled); + /* Finally we can run the query. */ spi_result = SPI_execute_snapshot(qplan, vals, nulls, @@ -3089,6 +3100,9 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo, /* Restore UID and security context */ SetUserIdAndSecContext(save_userid, save_sec_context); + /* Restore row-level security performing mode */ + setRowLevelSecurityMode(save_rls_mode); + /* Check result */ if (spi_result < 0) elog(ERROR, "SPI_execute_snapshot returned %d", spi_result); diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index c42765c..5ebd944 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -51,6 +51,7 @@ #include "catalog/namespace.h" #include "executor/executor.h" #include "executor/spi.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "optimizer/planmain.h" #include "optimizer/prep.h" @@ -664,6 +665,16 @@ CheckCachedPlan(CachedPlanSource *plansource) AcquireExecutorLocks(plan->stmt_list, true); /* + * If plan was constructed with assumption of a particular user-id, + * and it is different from the current one, the cached-plan shall + * be invalidated to construct suitable query plan. + */ + if (plan->is_valid && + OidIsValid(plan->planUserId) && + plan->planUserId == GetUserId()) + plan->is_valid = false; + + /* * If plan was transient, check to see if TransactionXmin has * advanced, and if so invalidate it. */ @@ -715,6 +726,8 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist, { CachedPlan *plan; List *plist; + ListCell *cell; + Oid planUserId = InvalidOid; bool snapshot_set; bool spi_pushed; MemoryContext plan_context; @@ -793,6 +806,24 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist, PopOverrideSearchPath(); /* + * Check whether the generated plan assumes a particular user-id, or not. + * In case when a valid user-id is recorded on PlannedStmt->planUserId, + * it should be kept and used to validation check of the cached plan + * under the "current" user-id. + */ + foreach (cell, plist) + { + PlannedStmt *pstmt = lfirst(cell); + + if (IsA(pstmt, PlannedStmt) && OidIsValid(pstmt->planUserId)) + { + Assert(!OidIsValid(planUserId) || planUserId == pstmt->planUserId); + + planUserId = pstmt->planUserId; + } + } + + /* * Make a dedicated memory context for the CachedPlan and its subsidiary * data. It's probably not going to be large, but just in case, use the * default maxsize parameter. It's transient for the moment. @@ -827,6 +858,7 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist, plan->context = plan_context; plan->is_saved = false; plan->is_valid = true; + plan->planUserId = planUserId; /* assign generation number to new plan */ plan->generation = ++(plansource->generation); diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c index 2e6776e..11051ed 100644 --- a/src/backend/utils/cache/relcache.c +++ b/src/backend/utils/cache/relcache.c @@ -48,6 +48,7 @@ #include "catalog/pg_opclass.h" #include "catalog/pg_proc.h" #include "catalog/pg_rewrite.h" +#include "catalog/pg_rowlevelsec.h" #include "catalog/pg_tablespace.h" #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" @@ -895,6 +896,11 @@ RelationBuildDesc(Oid targetRelId, bool insertIt) else relation->trigdesc = NULL; + if (relation->rd_rel->relhasrowlevelsec) + RelationBuildRowLevelSecurity(relation); + else + relation->rlsdesc = NULL; + /* * if it's an index, initialize index-related information */ @@ -1784,6 +1790,8 @@ RelationDestroyRelation(Relation relation) MemoryContextDelete(relation->rd_indexcxt); if (relation->rd_rulescxt) MemoryContextDelete(relation->rd_rulescxt); + if (relation->rlsdesc) + MemoryContextDelete(relation->rlsdesc->rlscxt); pfree(relation); } @@ -3023,7 +3031,13 @@ RelationCacheInitializePhase3(void) relation->rd_rel->relhastriggers = false; restart = true; } - + if (relation->rd_rel->relhasrowlevelsec && relation->rlsdesc == NULL) + { + RelationBuildRowLevelSecurity(relation); + if (relation->rlsdesc == NULL) + relation->rd_rel->relhasrowlevelsec = false; + restart = true; + } /* Release hold on the relation */ RelationDecrementReferenceCount(relation); @@ -4173,6 +4187,7 @@ load_relcache_init_file(bool shared) rel->rd_rules = NULL; rel->rd_rulescxt = NULL; rel->trigdesc = NULL; + rel->rlsdesc = NULL; rel->rd_indexprs = NIL; rel->rd_indpred = NIL; rel->rd_exclops = NULL; diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index 7d67287..0291590 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -3865,6 +3865,7 @@ getTables(Archive *fout, int *numTables) int i_reloptions; int i_toastreloptions; int i_reloftype; + int i_rlsqual; /* Make sure we are in proper schema */ selectSourceSchema(fout, "pg_catalog"); @@ -3889,7 +3890,45 @@ getTables(Archive *fout, int *numTables) * we cannot correctly identify inherited columns, owned sequences, etc. */ - if (fout->remoteVersion >= 90100) + if (fout->remoteVersion >= 90300) + { + /* + * Left join to pick up dependency info linking sequences to their + * owning column, if any (note this dependency is AUTO as of 8.2) + */ + appendPQExpBuffer(query, + "SELECT c.tableoid, c.oid, c.relname, " + "c.relacl, c.relkind, c.relnamespace, " + "(%s c.relowner) AS rolname, " + "c.relchecks, c.relhastriggers, " + "c.relhasindex, c.relhasrules, c.relhasoids, " + "c.relfrozenxid, tc.oid AS toid, " + "tc.relfrozenxid AS tfrozenxid, " + "c.relpersistence, " + "CASE WHEN c.reloftype <> 0 THEN c.reloftype::pg_catalog.regtype ELSE NULL END AS reloftype, " + "d.refobjid AS owning_tab, " + "d.refobjsubid AS owning_col, " + "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, " + "array_to_string(c.reloptions, ', ') AS reloptions, " + "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions, " + "pg_catalog.pg_get_expr(rls.rlsqual, rls.rlsrelid) AS rlsqual " + "FROM pg_class c " + "LEFT JOIN pg_depend d ON " + "(c.relkind = '%c' AND " + "d.classid = c.tableoid AND d.objid = c.oid AND " + "d.objsubid = 0 AND " + "d.refclassid = c.tableoid AND d.deptype = 'a') " + "LEFT JOIN pg_class tc ON (c.reltoastrelid = tc.oid) " + "LEFT JOIN pg_rowlevelsec rls ON (c.oid = rls.rlsrelid) " + "WHERE c.relkind in ('%c', '%c', '%c', '%c', '%c') " + "ORDER BY c.oid", + username_subquery, + RELKIND_SEQUENCE, + RELKIND_RELATION, RELKIND_SEQUENCE, + RELKIND_VIEW, RELKIND_COMPOSITE_TYPE, + RELKIND_FOREIGN_TABLE); + } + else if (fout->remoteVersion >= 90100) { /* * Left join to pick up dependency info linking sequences to their @@ -3909,7 +3948,8 @@ getTables(Archive *fout, int *numTables) "d.refobjsubid AS owning_col, " "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, " "array_to_string(c.reloptions, ', ') AS reloptions, " - "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions " + "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions, " + "NULL as rlsqual " "FROM pg_class c " "LEFT JOIN pg_depend d ON " "(c.relkind = '%c' AND " @@ -3945,7 +3985,8 @@ getTables(Archive *fout, int *numTables) "d.refobjsubid AS owning_col, " "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, " "array_to_string(c.reloptions, ', ') AS reloptions, " - "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions " + "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions, " + "NULL AS rlsqual " "FROM pg_class c " "LEFT JOIN pg_depend d ON " "(c.relkind = '%c' AND " @@ -3980,7 +4021,8 @@ getTables(Archive *fout, int *numTables) "d.refobjsubid AS owning_col, " "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, " "array_to_string(c.reloptions, ', ') AS reloptions, " - "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions " + "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions, " + "NULL AS rlsqual " "FROM pg_class c " "LEFT JOIN pg_depend d ON " "(c.relkind = '%c' AND " @@ -4015,7 +4057,8 @@ getTables(Archive *fout, int *numTables) "d.refobjsubid AS owning_col, " "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, " "array_to_string(c.reloptions, ', ') AS reloptions, " - "NULL AS toast_reloptions " + "NULL AS toast_reloptions, " + "NULL AS rlsqual " "FROM pg_class c " "LEFT JOIN pg_depend d ON " "(c.relkind = '%c' AND " @@ -4051,7 +4094,8 @@ getTables(Archive *fout, int *numTables) "d.refobjsubid AS owning_col, " "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, " "NULL AS reloptions, " - "NULL AS toast_reloptions " + "NULL AS toast_reloptions, " + "NULL AS rlsqual " "FROM pg_class c " "LEFT JOIN pg_depend d ON " "(c.relkind = '%c' AND " @@ -4086,7 +4130,8 @@ getTables(Archive *fout, int *numTables) "d.refobjsubid AS owning_col, " "NULL AS reltablespace, " "NULL AS reloptions, " - "NULL AS toast_reloptions " + "NULL AS toast_reloptions, " + "NULL AS rlsqual " "FROM pg_class c " "LEFT JOIN pg_depend d ON " "(c.relkind = '%c' AND " @@ -4117,7 +4162,8 @@ getTables(Archive *fout, int *numTables) "NULL::int4 AS owning_col, " "NULL AS reltablespace, " "NULL AS reloptions, " - "NULL AS toast_reloptions " + "NULL AS toast_reloptions, " + "NULL AS rlsqual " "FROM pg_class " "WHERE relkind IN ('%c', '%c', '%c') " "ORDER BY oid", @@ -4143,7 +4189,8 @@ getTables(Archive *fout, int *numTables) "NULL::int4 AS owning_col, " "NULL AS reltablespace, " "NULL AS reloptions, " - "NULL AS toast_reloptions " + "NULL AS toast_reloptions, " + "NULL AS rlsqual " "FROM pg_class " "WHERE relkind IN ('%c', '%c', '%c') " "ORDER BY oid", @@ -4179,7 +4226,8 @@ getTables(Archive *fout, int *numTables) "NULL::int4 AS owning_col, " "NULL AS reltablespace, " "NULL AS reloptions, " - "NULL AS toast_reloptions " + "NULL AS toast_reloptions, " + "NULL AS rlsqual " "FROM pg_class c " "WHERE relkind IN ('%c', '%c') " "ORDER BY oid", @@ -4227,6 +4275,7 @@ getTables(Archive *fout, int *numTables) i_reloptions = PQfnumber(res, "reloptions"); i_toastreloptions = PQfnumber(res, "toast_reloptions"); i_reloftype = PQfnumber(res, "reloftype"); + i_rlsqual = PQfnumber(res, "rlsqual"); if (lockWaitTimeout && fout->remoteVersion >= 70300) { @@ -4269,6 +4318,10 @@ getTables(Archive *fout, int *numTables) tblinfo[i].reloftype = NULL; else tblinfo[i].reloftype = pg_strdup(PQgetvalue(res, i, i_reloftype)); + if (PQgetisnull(res, i, i_rlsqual)) + tblinfo[i].rlsqual = NULL; + else + tblinfo[i].rlsqual = pg_strdup(PQgetvalue(res, i, i_rlsqual)); tblinfo[i].ncheck = atoi(PQgetvalue(res, i, i_relchecks)); if (PQgetisnull(res, i, i_owning_tab)) { @@ -12707,6 +12760,9 @@ dumpTableSchema(Archive *fout, TableInfo *tbinfo) } } } + if (tbinfo->rlsqual) + appendPQExpBuffer(q, "ALTER TABLE ONLY %s SET ROW LEVEL SECURITY %s;\n", + fmtId(tbinfo->dobj.name), tbinfo->rlsqual); if (binary_upgrade) binary_upgrade_extension_member(q, &tbinfo->dobj, labelq->data); diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index b44187bb..20803ab 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -255,6 +255,7 @@ typedef struct _tableInfo uint32 toast_frozenxid; /* for restore toast frozen xid */ int ncheck; /* # of CHECK expressions */ char *reloftype; /* underlying type for typed table */ + char *rlsqual; /* row-level security policy */ /* these two are set only if table is a sequence owned by a column: */ Oid owning_tab; /* OID of table owning sequence */ int owning_col; /* attr # of column owning sequence */ diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h index f0eb564..fef31b7 100644 --- a/src/include/catalog/dependency.h +++ b/src/include/catalog/dependency.h @@ -146,6 +146,7 @@ typedef enum ObjectClass OCLASS_USER_MAPPING, /* pg_user_mapping */ OCLASS_DEFACL, /* pg_default_acl */ OCLASS_EXTENSION, /* pg_extension */ + OCLASS_ROWLEVELSEC, /* pg_rowlevelsec */ MAX_OCLASS /* MUST BE LAST */ } ObjectClass; diff --git a/src/include/catalog/indexing.h b/src/include/catalog/indexing.h index 450ec25..5e4467d 100644 --- a/src/include/catalog/indexing.h +++ b/src/include/catalog/indexing.h @@ -306,6 +306,9 @@ DECLARE_UNIQUE_INDEX(pg_extension_name_index, 3081, on pg_extension using btree( DECLARE_UNIQUE_INDEX(pg_range_rngtypid_index, 3542, on pg_range using btree(rngtypid oid_ops)); #define RangeTypidIndexId 3542 +DECLARE_UNIQUE_INDEX(pg_rowlevelsec_relid_index, 3839, on pg_rowlevelsec using btree(rlsrelid oid_ops)); +#define RowLevelSecurityIndexId 3839 + /* last step of initialization script: build the indexes declared above */ BUILD_INDICES diff --git a/src/include/catalog/pg_class.h b/src/include/catalog/pg_class.h index f83ce80..5b6e38b 100644 --- a/src/include/catalog/pg_class.h +++ b/src/include/catalog/pg_class.h @@ -65,6 +65,7 @@ CATALOG(pg_class,1259) BKI_BOOTSTRAP BKI_ROWTYPE_OID(83) BKI_SCHEMA_MACRO bool relhaspkey; /* has (or has had) PRIMARY KEY index */ bool relhasrules; /* has (or has had) any rules */ bool relhastriggers; /* has (or has had) any TRIGGERs */ + bool relhasrowlevelsec; /* has (or has had) row-level security */ bool relhassubclass; /* has (or has had) derived classes */ TransactionId relfrozenxid; /* all Xids < this are frozen in this rel */ @@ -91,7 +92,7 @@ typedef FormData_pg_class *Form_pg_class; * ---------------- */ -#define Natts_pg_class 27 +#define Natts_pg_class 28 #define Anum_pg_class_relname 1 #define Anum_pg_class_relnamespace 2 #define Anum_pg_class_reltype 3 @@ -115,10 +116,11 @@ typedef FormData_pg_class *Form_pg_class; #define Anum_pg_class_relhaspkey 21 #define Anum_pg_class_relhasrules 22 #define Anum_pg_class_relhastriggers 23 -#define Anum_pg_class_relhassubclass 24 -#define Anum_pg_class_relfrozenxid 25 -#define Anum_pg_class_relacl 26 -#define Anum_pg_class_reloptions 27 +#define Anum_pg_class_relhasrowlevelsec 24 +#define Anum_pg_class_relhassubclass 25 +#define Anum_pg_class_relfrozenxid 26 +#define Anum_pg_class_relacl 27 +#define Anum_pg_class_reloptions 28 /* ---------------- * initial contents of pg_class @@ -130,13 +132,13 @@ typedef FormData_pg_class *Form_pg_class; */ /* Note: "3" in the relfrozenxid column stands for FirstNormalTransactionId */ -DATA(insert OID = 1247 ( pg_type PGNSP 71 0 PGUID 0 0 0 0 0 0 0 0 f f p r 30 0 t f f f f 3 _null_ _null_ )); +DATA(insert OID = 1247 ( pg_type PGNSP 71 0 PGUID 0 0 0 0 0 0 0 0 f f p r 30 0 t f f f f f 3 _null_ _null_ )); DESCR(""); -DATA(insert OID = 1249 ( pg_attribute PGNSP 75 0 PGUID 0 0 0 0 0 0 0 0 f f p r 21 0 f f f f f 3 _null_ _null_ )); +DATA(insert OID = 1249 ( pg_attribute PGNSP 75 0 PGUID 0 0 0 0 0 0 0 0 f f p r 21 0 f f f f f f 3 _null_ _null_ )); DESCR(""); -DATA(insert OID = 1255 ( pg_proc PGNSP 81 0 PGUID 0 0 0 0 0 0 0 0 f f p r 27 0 t f f f f 3 _null_ _null_ )); +DATA(insert OID = 1255 ( pg_proc PGNSP 81 0 PGUID 0 0 0 0 0 0 0 0 f f p r 27 0 t f f f f f 3 _null_ _null_ )); DESCR(""); -DATA(insert OID = 1259 ( pg_class PGNSP 83 0 PGUID 0 0 0 0 0 0 0 0 f f p r 27 0 t f f f f 3 _null_ _null_ )); +DATA(insert OID = 1259 ( pg_class PGNSP 83 0 PGUID 0 0 0 0 0 0 0 0 f f p r 28 0 t f f f f f 3 _null_ _null_ )); DESCR(""); diff --git a/src/include/catalog/pg_rowlevelsec.h b/src/include/catalog/pg_rowlevelsec.h new file mode 100644 index 0000000..5a64d1b --- /dev/null +++ b/src/include/catalog/pg_rowlevelsec.h @@ -0,0 +1,60 @@ +/* + * pg_rowlevelsec.h + * definition of the system catalog for row-level security policy + * (pg_rowlevelsec) + * + * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + */ +#ifndef PG_ROWLEVELSEC_H +#define PG_ROWLEVELSEC_H + +#include "catalog/genbki.h" +#include "nodes/primnodes.h" +#include "utils/memutils.h" +#include "utils/relcache.h" + +/* ---------------- + * pg_rowlevelsec definition. cpp turns this into + * typedef struct FormData_pg_rowlevelsec + * ---------------- + */ +#define RowLevelSecurityRelationId 3838 + +CATALOG(pg_rowlevelsec,3838) BKI_WITHOUT_OIDS +{ + Oid rlsrelid; +#ifdef CATALOG_VARLEN + pg_node_tree rlsqual; +#endif +} FormData_pg_rowlevelsec; + +/* ---------------- + * Form_pg_rowlevelsec corresponds to a pointer to a row with + * the format of pg_rowlevelsec relation. + * ---------------- + */ +typedef FormData_pg_rowlevelsec *Form_pg_rowlevelsec; + +/* ---------------- + * compiler constants for pg_rowlevelsec + * ---------------- + */ +#define Natts_pg_rowlevelsec 2 +#define Anum_pg_rowlevelsec_rlsrelid 1 +#define Anum_pg_rowlevelsec_rlsqual 2 + +typedef struct +{ + MemoryContext rlscxt; + Expr *rlsqual; + bool rlshassublinks; +} RowLevelSecDesc; + +extern void RelationBuildRowLevelSecurity(Relation relation); +extern void SetRowLevelSecurity(Relation relation, Node *clause); +extern void ResetRowLevelSecurity(Relation relation); +extern void RemoveRowLevelSecurityById(Oid relationId); + +#endif /* PG_ROWLEVELSEC_H */ diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 50111cb..de85fff 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -31,7 +31,8 @@ typedef enum QuerySource QSRC_PARSER, /* added by parse analysis (now unused) */ QSRC_INSTEAD_RULE, /* added by unconditional INSTEAD rule */ QSRC_QUAL_INSTEAD_RULE, /* added by conditional INSTEAD rule */ - QSRC_NON_INSTEAD_RULE /* added by non-INSTEAD rule */ + QSRC_NON_INSTEAD_RULE, /* added by non-INSTEAD rule */ + QSRC_ROW_LEVEL_SECURITY, /* added by row-level security */ } QuerySource; /* Sort ordering options for ORDER BY and CREATE INDEX */ @@ -1227,6 +1228,8 @@ typedef enum AlterTableType AT_DropInherit, /* NO INHERIT parent */ AT_AddOf, /* OF */ AT_DropOf, /* NOT OF */ + AT_SetRowLevelSecurity, /* SET ROW LEVEL SECURITY (...) */ + AT_ResetRowLevelSecurity, /* RESET ROW LEVEL SECURITY */ AT_GenericOptions /* OPTIONS (...) */ } AlterTableType; diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index fb9a863..6b3ea3d 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -67,6 +67,8 @@ typedef struct PlannedStmt List *invalItems; /* other dependencies, as PlanInvalItems */ int nParamExec; /* number of PARAM_EXEC Params used */ + + Oid planUserId; /* user-id this plan assumed, or InvalidOid */ } PlannedStmt; /* macro for fetching the Plan associated with a SubPlan node */ diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h index cf0bbd9..26f986a 100644 --- a/src/include/nodes/relation.h +++ b/src/include/nodes/relation.h @@ -98,6 +98,8 @@ typedef struct PlannerGlobal Index lastRowMarkId; /* highest PlanRowMark ID assigned */ bool transientPlan; /* redo plan when TransactionXmin changes? */ + + Oid planUserId; /* User-Id to be assumed on this plan */ } PlannerGlobal; /* macro for fetching the Plan associated with a SubPlan node */ diff --git a/src/include/optimizer/rowlevelsec.h b/src/include/optimizer/rowlevelsec.h new file mode 100644 index 0000000..9afe00a --- /dev/null +++ b/src/include/optimizer/rowlevelsec.h @@ -0,0 +1,31 @@ +/* ------------------------------------------------------------------------- + * + * rowlevelsec.h + * prototypes for optimizer/rowlevelsec.c + * + * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * ------------------------------------------------------------------------- + */ +#ifndef ROWLEVELSEC_H +#define ROWLEVELSEC_H + +#include "nodes/relation.h" +#include "utils/rel.h" + +typedef List *(*rowlevel_security_hook_type)(PlannerInfo *root, + Relation relation); +extern PGDLLIMPORT rowlevel_security_hook_type rowlevel_security_hook; + +typedef enum { + RowLevelSecModeEnabled, + RowLevelSecModeDisabled, +} RowLevelSecMode; + +extern RowLevelSecMode getRowLevelSecurityMode(void); +extern void setRowLevelSecurityMode(RowLevelSecMode new_mode); + +extern void applyRowLevelSecurity(PlannerInfo *root); + +#endif /* ROWLEVELSEC_H */ diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h index 413e846..5f89028 100644 --- a/src/include/utils/plancache.h +++ b/src/include/utils/plancache.h @@ -115,6 +115,8 @@ typedef struct CachedPlan * bare utility statements) */ bool is_saved; /* is CachedPlan in a long-lived context? */ bool is_valid; /* is the stmt_list currently valid? */ + Oid planUserId; /* is user-id that is assumed on this cached + plan, or InvalidOid if portable for anybody */ TransactionId saved_xmin; /* if valid, replan when TransactionXmin * changes from this value */ int generation; /* parent's generation number for this plan */ diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h index 4669d8a..dce8463 100644 --- a/src/include/utils/rel.h +++ b/src/include/utils/rel.h @@ -18,6 +18,7 @@ #include "catalog/pg_am.h" #include "catalog/pg_class.h" #include "catalog/pg_index.h" +#include "catalog/pg_rowlevelsec.h" #include "fmgr.h" #include "nodes/bitmapset.h" #include "rewrite/prs2lock.h" @@ -109,6 +110,7 @@ typedef struct RelationData RuleLock *rd_rules; /* rewrite rules */ MemoryContext rd_rulescxt; /* private memory cxt for rd_rules, if any */ TriggerDesc *trigdesc; /* Trigger info, or NULL if rel has none */ + RowLevelSecDesc *rlsdesc; /* Row-level security info, or NULL */ /* * rd_options is set whenever rd_rel is loaded into the relcache entry. diff --git a/src/test/regress/expected/rowlevelsec.out b/src/test/regress/expected/rowlevelsec.out new file mode 100644 index 0000000..5673e7a --- /dev/null +++ b/src/test/regress/expected/rowlevelsec.out @@ -0,0 +1,753 @@ +-- +-- Test of Row-level security feature +-- +-- Clean up in case a prior regression run failed +-- Suppress NOTICE messages when users/groups don't exist +SET client_min_messages TO 'warning'; +DROP USER IF EXISTS rls_regress_user0; +DROP USER IF EXISTS rls_regress_user1; +DROP USER IF EXISTS rls_regress_user2; +DROP SCHEMA IF EXISTS rls_regress_schema CASCADE; +RESET client_min_messages; +-- initial setup +CREATE USER rls_regress_user0; +CREATE USER rls_regress_user1; +CREATE USER rls_regress_user2; +CREATE SCHEMA rls_regress_schema; +GRANT ALL ON SCHEMA rls_regress_schema TO public; +SET search_path = rls_regress_schema; +CREATE TABLE account ( + pguser name primary key, + slevel int, + scategory bit(4) +); +GRANT SELECT,REFERENCES ON TABLE account TO public; +INSERT INTO account VALUES + ('rls_regress_user1', 1, B'0011'), + ('rls_regress_user2', 2, B'0110'), + ('rls_regress_user3', 0, B'0101'); +CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool + COST 0.0000001 LANGUAGE plpgsql + AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END'; +GRANT EXECUTE ON FUNCTION f_leak(text) TO public; +-- Creation of Test Data +SET SESSION AUTHORIZATION rls_regress_user0; +CREATE TABLE document ( + did int primary key, + dlevel int, + dcategory bit(4), + dtitle text +); +GRANT ALL ON document TO public; +INSERT INTO document VALUES + ( 10, 0, B'0000', 'this document is unclassified, category(----)'), + ( 20, 0, B'0001', 'this document is unclassified, category(---A)'), + ( 30, 0, B'0010', 'this document is unclassified, category(--B-)'), + ( 40, 0, B'0011', 'this document is unclassified, category(--BA)'), + ( 50, 0, B'0100', 'this document is unclassified, category(-C--)'), + ( 60, 0, B'0101', 'this document is unclassified, category(-C-A)'), + ( 70, 0, B'0110', 'this document is unclassified, category(-CB-)'), + ( 80, 0, B'0111', 'this document is unclassified, category(-CBA)'), + ( 90, 1, B'0000', 'this document is classified, category(----)'), + (100, 1, B'0001', 'this document is classified, category(---A)'), + (110, 1, B'0010', 'this document is classified, category(--B-)'), + (120, 1, B'0011', 'this document is classified, category(--BA)'), + (130, 1, B'0100', 'this document is classified, category(-C--)'), + (140, 1, B'0101', 'this document is classified, category(-C-A)'), + (150, 1, B'0110', 'this document is classified, category(-CB-)'), + (160, 1, B'0111', 'this document is classified, category(-CBA)'), + (170, 2, B'0000', 'this document is secret, category(----)'), + (180, 2, B'0001', 'this document is secret, category(---A)'), + (190, 2, B'0010', 'this document is secret, category(--B-)'), + (200, 2, B'0011', 'this document is secret, category(--BA)'), + (210, 2, B'0100', 'this document is secret, category(-C--)'), + (220, 2, B'0101', 'this document is secret, category(-C-A)'), + (230, 2, B'0110', 'this document is secret, category(-CB-)'), + (240, 2, B'0111', 'this document is secret, category(-CBA)'); +CREATE TABLE browse ( + pguser name references account(pguser), + did int references document(did), + ymd date +); +GRANT ALL ON browse TO public; +INSERT INTO browse VALUES + ('rls_regress_user1', 20, '2012-07-01'), + ('rls_regress_user1', 40, '2012-07-02'), + ('rls_regress_user1', 110, '2012-07-03'), + ('rls_regress_user2', 30, '2012-07-04'), + ('rls_regress_user2', 50, '2012-07-05'), + ('rls_regress_user2', 90, '2012-07-06'), + ('rls_regress_user2', 130, '2012-07-07'), + ('rls_regress_user2', 150, '2012-07-08'), + ('rls_regress_user2', 150, '2012-07-08'), + ('rls_regress_user2', 190, '2012-07-09'), + ('rls_regress_user2', 210, '2012-07-10'), + ('rls_regress_user3', 10, '2012-07-11'), + ('rls_regress_user3', 50, '2012-07-12'); +-- user's sensitivity level must higher than document's level +ALTER TABLE document SET ROW LEVEL SECURITY + (dlevel <= (SELECT slevel FROM account WHERE pguser = current_user)); +ALTER TABLE browse SET ROW LEVEL SECURITY + (pguser = current_user); +-- viewpoint from rls_regress_user1 +SET SESSION AUTHORIZATION rls_regress_user1; +SELECT * FROM document WHERE f_leak(dtitle); +NOTICE: f_leak => this document is unclassified, category(----) +NOTICE: f_leak => this document is unclassified, category(---A) +NOTICE: f_leak => this document is unclassified, category(--B-) +NOTICE: f_leak => this document is unclassified, category(--BA) +NOTICE: f_leak => this document is unclassified, category(-C--) +NOTICE: f_leak => this document is unclassified, category(-C-A) +NOTICE: f_leak => this document is unclassified, category(-CB-) +NOTICE: f_leak => this document is unclassified, category(-CBA) +NOTICE: f_leak => this document is classified, category(----) +NOTICE: f_leak => this document is classified, category(---A) +NOTICE: f_leak => this document is classified, category(--B-) +NOTICE: f_leak => this document is classified, category(--BA) +NOTICE: f_leak => this document is classified, category(-C--) +NOTICE: f_leak => this document is classified, category(-C-A) +NOTICE: f_leak => this document is classified, category(-CB-) +NOTICE: f_leak => this document is classified, category(-CBA) + did | dlevel | dcategory | dtitle +-----+--------+-----------+----------------------------------------------- + 10 | 0 | 0000 | this document is unclassified, category(----) + 20 | 0 | 0001 | this document is unclassified, category(---A) + 30 | 0 | 0010 | this document is unclassified, category(--B-) + 40 | 0 | 0011 | this document is unclassified, category(--BA) + 50 | 0 | 0100 | this document is unclassified, category(-C--) + 60 | 0 | 0101 | this document is unclassified, category(-C-A) + 70 | 0 | 0110 | this document is unclassified, category(-CB-) + 80 | 0 | 0111 | this document is unclassified, category(-CBA) + 90 | 1 | 0000 | this document is classified, category(----) + 100 | 1 | 0001 | this document is classified, category(---A) + 110 | 1 | 0010 | this document is classified, category(--B-) + 120 | 1 | 0011 | this document is classified, category(--BA) + 130 | 1 | 0100 | this document is classified, category(-C--) + 140 | 1 | 0101 | this document is classified, category(-C-A) + 150 | 1 | 0110 | this document is classified, category(-CB-) + 160 | 1 | 0111 | this document is classified, category(-CBA) +(16 rows) + +SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text); +NOTICE: f_leak => (rls_regress_user1,20,07-01-2012) +NOTICE: f_leak => (rls_regress_user1,40,07-02-2012) +NOTICE: f_leak => (rls_regress_user1,110,07-03-2012) + did | dlevel | dcategory | dtitle | pguser | ymd +-----+--------+-----------+-----------------------------------------------+-------------------+------------ + 20 | 0 | 0001 | this document is unclassified, category(---A) | rls_regress_user1 | 07-01-2012 + 40 | 0 | 0011 | this document is unclassified, category(--BA) | rls_regress_user1 | 07-02-2012 + 110 | 1 | 0010 | this document is classified, category(--B-) | rls_regress_user1 | 07-03-2012 +(3 rows) + +-- viewpoint from rls_regress_user2 +SET SESSION AUTHORIZATION rls_regress_user2; +SELECT * FROM document WHERE f_leak(dtitle); +NOTICE: f_leak => this document is unclassified, category(----) +NOTICE: f_leak => this document is unclassified, category(---A) +NOTICE: f_leak => this document is unclassified, category(--B-) +NOTICE: f_leak => this document is unclassified, category(--BA) +NOTICE: f_leak => this document is unclassified, category(-C--) +NOTICE: f_leak => this document is unclassified, category(-C-A) +NOTICE: f_leak => this document is unclassified, category(-CB-) +NOTICE: f_leak => this document is unclassified, category(-CBA) +NOTICE: f_leak => this document is classified, category(----) +NOTICE: f_leak => this document is classified, category(---A) +NOTICE: f_leak => this document is classified, category(--B-) +NOTICE: f_leak => this document is classified, category(--BA) +NOTICE: f_leak => this document is classified, category(-C--) +NOTICE: f_leak => this document is classified, category(-C-A) +NOTICE: f_leak => this document is classified, category(-CB-) +NOTICE: f_leak => this document is classified, category(-CBA) +NOTICE: f_leak => this document is secret, category(----) +NOTICE: f_leak => this document is secret, category(---A) +NOTICE: f_leak => this document is secret, category(--B-) +NOTICE: f_leak => this document is secret, category(--BA) +NOTICE: f_leak => this document is secret, category(-C--) +NOTICE: f_leak => this document is secret, category(-C-A) +NOTICE: f_leak => this document is secret, category(-CB-) +NOTICE: f_leak => this document is secret, category(-CBA) + did | dlevel | dcategory | dtitle +-----+--------+-----------+----------------------------------------------- + 10 | 0 | 0000 | this document is unclassified, category(----) + 20 | 0 | 0001 | this document is unclassified, category(---A) + 30 | 0 | 0010 | this document is unclassified, category(--B-) + 40 | 0 | 0011 | this document is unclassified, category(--BA) + 50 | 0 | 0100 | this document is unclassified, category(-C--) + 60 | 0 | 0101 | this document is unclassified, category(-C-A) + 70 | 0 | 0110 | this document is unclassified, category(-CB-) + 80 | 0 | 0111 | this document is unclassified, category(-CBA) + 90 | 1 | 0000 | this document is classified, category(----) + 100 | 1 | 0001 | this document is classified, category(---A) + 110 | 1 | 0010 | this document is classified, category(--B-) + 120 | 1 | 0011 | this document is classified, category(--BA) + 130 | 1 | 0100 | this document is classified, category(-C--) + 140 | 1 | 0101 | this document is classified, category(-C-A) + 150 | 1 | 0110 | this document is classified, category(-CB-) + 160 | 1 | 0111 | this document is classified, category(-CBA) + 170 | 2 | 0000 | this document is secret, category(----) + 180 | 2 | 0001 | this document is secret, category(---A) + 190 | 2 | 0010 | this document is secret, category(--B-) + 200 | 2 | 0011 | this document is secret, category(--BA) + 210 | 2 | 0100 | this document is secret, category(-C--) + 220 | 2 | 0101 | this document is secret, category(-C-A) + 230 | 2 | 0110 | this document is secret, category(-CB-) + 240 | 2 | 0111 | this document is secret, category(-CBA) +(24 rows) + +SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text); +NOTICE: f_leak => (rls_regress_user2,30,07-04-2012) +NOTICE: f_leak => (rls_regress_user2,50,07-05-2012) +NOTICE: f_leak => (rls_regress_user2,90,07-06-2012) +NOTICE: f_leak => (rls_regress_user2,130,07-07-2012) +NOTICE: f_leak => (rls_regress_user2,150,07-08-2012) +NOTICE: f_leak => (rls_regress_user2,150,07-08-2012) +NOTICE: f_leak => (rls_regress_user2,190,07-09-2012) +NOTICE: f_leak => (rls_regress_user2,210,07-10-2012) + did | dlevel | dcategory | dtitle | pguser | ymd +-----+--------+-----------+-----------------------------------------------+-------------------+------------ + 30 | 0 | 0010 | this document is unclassified, category(--B-) | rls_regress_user2 | 07-04-2012 + 50 | 0 | 0100 | this document is unclassified, category(-C--) | rls_regress_user2 | 07-05-2012 + 90 | 1 | 0000 | this document is classified, category(----) | rls_regress_user2 | 07-06-2012 + 130 | 1 | 0100 | this document is classified, category(-C--) | rls_regress_user2 | 07-07-2012 + 150 | 1 | 0110 | this document is classified, category(-CB-) | rls_regress_user2 | 07-08-2012 + 150 | 1 | 0110 | this document is classified, category(-CB-) | rls_regress_user2 | 07-08-2012 + 190 | 2 | 0010 | this document is secret, category(--B-) | rls_regress_user2 | 07-09-2012 + 210 | 2 | 0100 | this document is secret, category(-C--) | rls_regress_user2 | 07-10-2012 +(8 rows) + +EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle); + QUERY PLAN +--------------------------------------------------------- + Subquery Scan on document + Filter: f_leak(document.dtitle) + -> Seq Scan on document + Filter: (dlevel <= $0) + InitPlan 1 (returns $0) + -> Index Scan using account_pkey on account + Index Cond: (pguser = "current_user"()) +(7 rows) + +EXPLAIN SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text); + QUERY PLAN +------------------------------------------------------------------------------------------ + Hash Join (cost=30.34..57.95 rows=2 width=117) + Hash Cond: (rls_regress_schema.document.did = browse.did) + -> Seq Scan on document (cost=8.27..31.15 rows=343 width=49) + Filter: (dlevel <= $0) + InitPlan 1 (returns $0) + -> Index Scan using account_pkey on account (cost=0.00..8.27 rows=1 width=4) + Index Cond: (pguser = "current_user"()) + -> Hash (cost=22.06..22.06 rows=1 width=72) + -> Subquery Scan on browse (cost=0.00..22.06 rows=1 width=72) + Filter: f_leak((browse.browse)::text) + -> Seq Scan on browse (cost=0.00..22.00 rows=4 width=168) + Filter: (pguser = "current_user"()) +(12 rows) + +-- change row-level security policy +ALTER TABLE document RESET ROW LEVEL SECURITY; -- failed +ERROR: must be owner of relation document +ALTER TABLE document SET ROW LEVEL SECURITY (true); -- failed +ERROR: must be owner of relation document +SET SESSION AUTHORIZATION rls_regress_user0; +-- switch to category based control from level based +ALTER TABLE document SET ROW LEVEL SECURITY + (dcategory & (SELECT ~scategory FROM account WHERE pguser = current_user) = B'0000'); +-- viewpoint from rls_regress_user1 again +SET SESSION AUTHORIZATION rls_regress_user1; +SELECT * FROM document WHERE f_leak(dtitle); +NOTICE: f_leak => this document is unclassified, category(----) +NOTICE: f_leak => this document is unclassified, category(---A) +NOTICE: f_leak => this document is unclassified, category(--B-) +NOTICE: f_leak => this document is unclassified, category(--BA) +NOTICE: f_leak => this document is classified, category(----) +NOTICE: f_leak => this document is classified, category(---A) +NOTICE: f_leak => this document is classified, category(--B-) +NOTICE: f_leak => this document is classified, category(--BA) +NOTICE: f_leak => this document is secret, category(----) +NOTICE: f_leak => this document is secret, category(---A) +NOTICE: f_leak => this document is secret, category(--B-) +NOTICE: f_leak => this document is secret, category(--BA) + did | dlevel | dcategory | dtitle +-----+--------+-----------+----------------------------------------------- + 10 | 0 | 0000 | this document is unclassified, category(----) + 20 | 0 | 0001 | this document is unclassified, category(---A) + 30 | 0 | 0010 | this document is unclassified, category(--B-) + 40 | 0 | 0011 | this document is unclassified, category(--BA) + 90 | 1 | 0000 | this document is classified, category(----) + 100 | 1 | 0001 | this document is classified, category(---A) + 110 | 1 | 0010 | this document is classified, category(--B-) + 120 | 1 | 0011 | this document is classified, category(--BA) + 170 | 2 | 0000 | this document is secret, category(----) + 180 | 2 | 0001 | this document is secret, category(---A) + 190 | 2 | 0010 | this document is secret, category(--B-) + 200 | 2 | 0011 | this document is secret, category(--BA) +(12 rows) + +SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text); +NOTICE: f_leak => (rls_regress_user1,20,07-01-2012) +NOTICE: f_leak => (rls_regress_user1,40,07-02-2012) +NOTICE: f_leak => (rls_regress_user1,110,07-03-2012) + did | dlevel | dcategory | dtitle | pguser | ymd +-----+--------+-----------+-----------------------------------------------+-------------------+------------ + 20 | 0 | 0001 | this document is unclassified, category(---A) | rls_regress_user1 | 07-01-2012 + 40 | 0 | 0011 | this document is unclassified, category(--BA) | rls_regress_user1 | 07-02-2012 + 110 | 1 | 0010 | this document is classified, category(--B-) | rls_regress_user1 | 07-03-2012 +(3 rows) + +-- viewpoint from rls_regress_user2 again +SET SESSION AUTHORIZATION rls_regress_user2; +SELECT * FROM document WHERE f_leak(dtitle); +NOTICE: f_leak => this document is unclassified, category(----) +NOTICE: f_leak => this document is unclassified, category(--B-) +NOTICE: f_leak => this document is unclassified, category(-C--) +NOTICE: f_leak => this document is unclassified, category(-CB-) +NOTICE: f_leak => this document is classified, category(----) +NOTICE: f_leak => this document is classified, category(--B-) +NOTICE: f_leak => this document is classified, category(-C--) +NOTICE: f_leak => this document is classified, category(-CB-) +NOTICE: f_leak => this document is secret, category(----) +NOTICE: f_leak => this document is secret, category(--B-) +NOTICE: f_leak => this document is secret, category(-C--) +NOTICE: f_leak => this document is secret, category(-CB-) + did | dlevel | dcategory | dtitle +-----+--------+-----------+----------------------------------------------- + 10 | 0 | 0000 | this document is unclassified, category(----) + 30 | 0 | 0010 | this document is unclassified, category(--B-) + 50 | 0 | 0100 | this document is unclassified, category(-C--) + 70 | 0 | 0110 | this document is unclassified, category(-CB-) + 90 | 1 | 0000 | this document is classified, category(----) + 110 | 1 | 0010 | this document is classified, category(--B-) + 130 | 1 | 0100 | this document is classified, category(-C--) + 150 | 1 | 0110 | this document is classified, category(-CB-) + 170 | 2 | 0000 | this document is secret, category(----) + 190 | 2 | 0010 | this document is secret, category(--B-) + 210 | 2 | 0100 | this document is secret, category(-C--) + 230 | 2 | 0110 | this document is secret, category(-CB-) +(12 rows) + +SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text); +NOTICE: f_leak => (rls_regress_user2,30,07-04-2012) +NOTICE: f_leak => (rls_regress_user2,50,07-05-2012) +NOTICE: f_leak => (rls_regress_user2,90,07-06-2012) +NOTICE: f_leak => (rls_regress_user2,130,07-07-2012) +NOTICE: f_leak => (rls_regress_user2,150,07-08-2012) +NOTICE: f_leak => (rls_regress_user2,150,07-08-2012) +NOTICE: f_leak => (rls_regress_user2,190,07-09-2012) +NOTICE: f_leak => (rls_regress_user2,210,07-10-2012) + did | dlevel | dcategory | dtitle | pguser | ymd +-----+--------+-----------+-----------------------------------------------+-------------------+------------ + 30 | 0 | 0010 | this document is unclassified, category(--B-) | rls_regress_user2 | 07-04-2012 + 50 | 0 | 0100 | this document is unclassified, category(-C--) | rls_regress_user2 | 07-05-2012 + 90 | 1 | 0000 | this document is classified, category(----) | rls_regress_user2 | 07-06-2012 + 130 | 1 | 0100 | this document is classified, category(-C--) | rls_regress_user2 | 07-07-2012 + 150 | 1 | 0110 | this document is classified, category(-CB-) | rls_regress_user2 | 07-08-2012 + 150 | 1 | 0110 | this document is classified, category(-CB-) | rls_regress_user2 | 07-08-2012 + 190 | 2 | 0010 | this document is secret, category(--B-) | rls_regress_user2 | 07-09-2012 + 210 | 2 | 0100 | this document is secret, category(-C--) | rls_regress_user2 | 07-10-2012 +(8 rows) + +EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle); + QUERY PLAN +--------------------------------------------------------- + Subquery Scan on document + Filter: f_leak(document.dtitle) + -> Seq Scan on document + Filter: ((dcategory & $0) = B'0000'::"bit") + InitPlan 1 (returns $0) + -> Index Scan using account_pkey on account + Index Cond: (pguser = "current_user"()) +(7 rows) + +EXPLAIN SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text); + QUERY PLAN +------------------------------------------------------------------------------------------ + Nested Loop (cost=8.27..55.90 rows=1 width=117) + Join Filter: (rls_regress_schema.document.did = browse.did) + -> Subquery Scan on browse (cost=0.00..22.06 rows=1 width=72) + Filter: f_leak((browse.browse)::text) + -> Seq Scan on browse (cost=0.00..22.00 rows=4 width=168) + Filter: (pguser = "current_user"()) + -> Seq Scan on document (cost=8.27..33.72 rows=5 width=49) + Filter: ((dcategory & $0) = B'0000'::"bit") + InitPlan 1 (returns $0) + -> Index Scan using account_pkey on account (cost=0.00..8.27 rows=1 width=9) + Index Cond: (pguser = "current_user"()) +(11 rows) + +-- Failed to update PK row referenced by invisible FK +SET SESSION AUTHORIZATION rls_regress_user1; +SELECT * FROM document NATURAL LEFT JOIN browse; + did | dlevel | dcategory | dtitle | pguser | ymd +-----+--------+-----------+-----------------------------------------------+-------------------+------------ + 10 | 0 | 0000 | this document is unclassified, category(----) | | + 20 | 0 | 0001 | this document is unclassified, category(---A) | rls_regress_user1 | 07-01-2012 + 30 | 0 | 0010 | this document is unclassified, category(--B-) | | + 40 | 0 | 0011 | this document is unclassified, category(--BA) | rls_regress_user1 | 07-02-2012 + 90 | 1 | 0000 | this document is classified, category(----) | | + 100 | 1 | 0001 | this document is classified, category(---A) | | + 110 | 1 | 0010 | this document is classified, category(--B-) | rls_regress_user1 | 07-03-2012 + 120 | 1 | 0011 | this document is classified, category(--BA) | | + 170 | 2 | 0000 | this document is secret, category(----) | | + 180 | 2 | 0001 | this document is secret, category(---A) | | + 190 | 2 | 0010 | this document is secret, category(--B-) | | + 200 | 2 | 0011 | this document is secret, category(--BA) | | +(12 rows) + +DELETE FROM document WHERE did = 30; -- failed +ERROR: update or delete on table "document" violates foreign key constraint "browse_did_fkey" on table "browse" +DETAIL: Key (did)=(30) is still referenced from table "browse". +UPDATE document SET did = 9999 WHERE did = 90; -- failed +ERROR: update or delete on table "document" violates foreign key constraint "browse_did_fkey" on table "browse" +DETAIL: Key (did)=(90) is still referenced from table "browse". +-- +-- Table inheritance and RLS policy +-- +SET SESSION AUTHORIZATION rls_regress_user0; +CREATE TABLE t1 (a int, b text, c text) WITH OIDS; +ALTER TABLE t1 DROP COLUMN b; -- just a disturbing factor +GRANT ALL ON t1 TO public; +COPY t1 FROM stdin WITH (oids); +CREATE TABLE t2 (d float) INHERITS (t1); +COPY t2 FROM stdin WITH (oids); +CREATE TABLE t3 (e text) INHERITS (t1); +COPY t3 FROM stdin WITH (oids); +ALTER TABLE t1 SET ROW LEVEL SECURITY (a % 2 = 0); -- be even number +ALTER TABLE t2 SET ROW LEVEL SECURITY (a % 2 = 1); -- be odd number +SELECT * FROM t1; + a | c +---+----- + 2 | bbb + 4 | ddd + 1 | abc + 3 | cde + 1 | xxx + 2 | yyy + 3 | zzz +(7 rows) + +EXPLAIN (costs off) SELECT * FROM t1; + QUERY PLAN +------------------------------------- + Result + -> Append + -> Seq Scan on t1 + Filter: ((a % 2) = 0) + -> Seq Scan on t2 + Filter: ((a % 2) = 1) + -> Seq Scan on t3 +(7 rows) + +SELECT * FROM t1 WHERE f_leak(c); +NOTICE: f_leak => bbb +NOTICE: f_leak => ddd +NOTICE: f_leak => abc +NOTICE: f_leak => cde +NOTICE: f_leak => xxx +NOTICE: f_leak => yyy +NOTICE: f_leak => zzz + a | c +---+----- + 2 | bbb + 4 | ddd + 1 | abc + 3 | cde + 1 | xxx + 2 | yyy + 3 | zzz +(7 rows) + +EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(c); + QUERY PLAN +------------------------------------------- + Result + -> Append + -> Subquery Scan on t1 + Filter: f_leak(t1.c) + -> Seq Scan on t1 + Filter: ((a % 2) = 0) + -> Subquery Scan on t2 + Filter: f_leak(t2.c) + -> Seq Scan on t2 + Filter: ((a % 2) = 1) + -> Seq Scan on t3 + Filter: f_leak(c) +(12 rows) + +-- reference to system column +SELECT oid, * FROM t1; + oid | a | c +-----+---+----- + 102 | 2 | bbb + 104 | 4 | ddd + 201 | 1 | abc + 203 | 3 | cde + 301 | 1 | xxx + 302 | 2 | yyy + 303 | 3 | zzz +(7 rows) + +EXPLAIN (costs off) SELECT * FROM t1; + QUERY PLAN +------------------------------------- + Result + -> Append + -> Seq Scan on t1 + Filter: ((a % 2) = 0) + -> Seq Scan on t2 + Filter: ((a % 2) = 1) + -> Seq Scan on t3 +(7 rows) + +-- reference to whole-row reference +SELECT *,t1 FROM t1; + a | c | t1 +---+-----+--------- + 2 | bbb | (2,bbb) + 4 | ddd | (4,ddd) + 1 | abc | (1,abc) + 3 | cde | (3,cde) + 1 | xxx | (1,xxx) + 2 | yyy | (2,yyy) + 3 | zzz | (3,zzz) +(7 rows) + +EXPLAIN (costs off) SELECT *,t1 FROM t1; + QUERY PLAN +------------------------------------------- + Result + -> Append + -> Subquery Scan on t1 + -> Seq Scan on t1 + Filter: ((a % 2) = 0) + -> Subquery Scan on t2 + -> Seq Scan on t2 + Filter: ((a % 2) = 1) + -> Subquery Scan on t3 + -> Seq Scan on t3 +(10 rows) + +-- for share/update lock +SELECT * FROM t1 FOR SHARE; + a | c +---+----- + 2 | bbb + 4 | ddd + 1 | abc + 3 | cde + 1 | xxx + 2 | yyy + 3 | zzz +(7 rows) + +EXPLAIN (costs off) SELECT * FROM t1 FOR SHARE; + QUERY PLAN +------------------------------------------------- + Result + -> Append + -> Subquery Scan on t1 + -> LockRows + -> Seq Scan on t1 + Filter: ((a % 2) = 0) + -> Subquery Scan on t2 + -> LockRows + -> Seq Scan on t2 + Filter: ((a % 2) = 1) + -> Subquery Scan on t3 + -> LockRows + -> Seq Scan on t3 +(13 rows) + +SELECT * FROM t1 WHERE f_leak(c) FOR SHARE; +NOTICE: f_leak => bbb +NOTICE: f_leak => ddd +NOTICE: f_leak => abc +NOTICE: f_leak => cde +NOTICE: f_leak => xxx +NOTICE: f_leak => yyy +NOTICE: f_leak => zzz + a | c +---+----- + 2 | bbb + 4 | ddd + 1 | abc + 3 | cde + 1 | xxx + 2 | yyy + 3 | zzz +(7 rows) + +EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(c) FOR SHARE; + QUERY PLAN +------------------------------------------------- + Result + -> Append + -> Subquery Scan on t1 + Filter: f_leak(t1.c) + -> LockRows + -> Seq Scan on t1 + Filter: ((a % 2) = 0) + -> Subquery Scan on t2 + Filter: f_leak(t2.c) + -> LockRows + -> Seq Scan on t2 + Filter: ((a % 2) = 1) + -> Subquery Scan on t3 + -> LockRows + -> Seq Scan on t3 + Filter: f_leak(c) +(16 rows) + +-- Now COPY TO command does not support RLS +-- COPY t1 TO stdin; +-- prepared statement with rls_regress_user0 privilege +PREPARE p1(int) AS SELECT * FROM t1 WHERE a <= $1; +EXECUTE p1(2); + a | c +---+----- + 2 | bbb + 1 | abc + 1 | xxx + 2 | yyy +(4 rows) + +EXPLAIN (costs off) EXECUTE p1(2); + QUERY PLAN +---------------------------------------------------- + Result + -> Append + -> Seq Scan on t1 + Filter: ((a <= 2) AND ((a % 2) = 0)) + -> Seq Scan on t2 + Filter: ((a <= 2) AND ((a % 2) = 1)) + -> Seq Scan on t3 + Filter: (a <= 2) +(8 rows) + +-- superuser is allowed to bypass RLS checks +RESET SESSION AUTHORIZATION; +SELECT * FROM t1 WHERE f_leak(c); +NOTICE: f_leak => aaa +NOTICE: f_leak => bbb +NOTICE: f_leak => ccc +NOTICE: f_leak => ddd +NOTICE: f_leak => abc +NOTICE: f_leak => bcd +NOTICE: f_leak => cde +NOTICE: f_leak => def +NOTICE: f_leak => xxx +NOTICE: f_leak => yyy +NOTICE: f_leak => zzz + a | c +---+----- + 1 | aaa + 2 | bbb + 3 | ccc + 4 | ddd + 1 | abc + 2 | bcd + 3 | cde + 4 | def + 1 | xxx + 2 | yyy + 3 | zzz +(11 rows) + +EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(c); + QUERY PLAN +--------------------------------- + Result + -> Append + -> Seq Scan on t1 + Filter: f_leak(c) + -> Seq Scan on t2 t1 + Filter: f_leak(c) + -> Seq Scan on t3 t1 + Filter: f_leak(c) +(8 rows) + +-- plan cache should be invalidated +EXECUTE p1(2); + a | c +---+----- + 1 | aaa + 2 | bbb + 1 | abc + 2 | bcd + 1 | xxx + 2 | yyy +(6 rows) + +EXPLAIN (costs off) EXECUTE p1(2); + QUERY PLAN +-------------------------------- + Result + -> Append + -> Seq Scan on t1 + Filter: (a <= 2) + -> Seq Scan on t2 t1 + Filter: (a <= 2) + -> Seq Scan on t3 t1 + Filter: (a <= 2) +(8 rows) + +PREPARE p2(int) AS SELECT * FROM t1 WHERE a = $1; +EXECUTE p2(2); + a | c +---+----- + 2 | bbb + 2 | bcd + 2 | yyy +(3 rows) + +EXPLAIN (costs off) EXECUTE p2(2); + QUERY PLAN +------------------------------- + Result + -> Append + -> Seq Scan on t1 + Filter: (a = 2) + -> Seq Scan on t2 t1 + Filter: (a = 2) + -> Seq Scan on t3 t1 + Filter: (a = 2) +(8 rows) + +-- also, case when privilege switch from superuser +SET SESSION AUTHORIZATION rls_regress_user0; +EXECUTE p2(2); + a | c +---+----- + 2 | bbb + 2 | yyy +(2 rows) + +EXPLAIN (costs off) EXECUTE p2(2); + QUERY PLAN +--------------------------------------------------- + Result + -> Append + -> Seq Scan on t1 + Filter: ((a = 2) AND ((a % 2) = 0)) + -> Seq Scan on t2 + Filter: ((a = 2) AND ((a % 2) = 1)) + -> Seq Scan on t3 + Filter: (a = 2) +(8 rows) + +-- +-- Clean up objects +-- +RESET SESSION AUTHORIZATION; +DROP SCHEMA rls_regress_schema CASCADE; +NOTICE: drop cascades to 7 other objects +DETAIL: drop cascades to table account +drop cascades to function f_leak(text) +drop cascades to table document +drop cascades to table browse +drop cascades to table t1 +drop cascades to table t2 +drop cascades to table t3 +DROP USER rls_regress_user0; +DROP USER rls_regress_user1; +DROP USER rls_regress_user2; diff --git a/src/test/regress/expected/sanity_check.out b/src/test/regress/expected/sanity_check.out index 7f560d2..fd50d9e 100644 --- a/src/test/regress/expected/sanity_check.out +++ b/src/test/regress/expected/sanity_check.out @@ -119,6 +119,7 @@ SELECT relname, relhasindex pg_proc | t pg_range | t pg_rewrite | t + pg_rowlevelsec | t pg_seclabel | t pg_shdepend | t pg_shdescription | t @@ -164,7 +165,7 @@ SELECT relname, relhasindex timetz_tbl | f tinterval_tbl | f varchar_tbl | f -(153 rows) +(154 rows) -- -- another sanity check: every system catalog that has OIDs should have diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 8852e0a..d368504 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -83,7 +83,7 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi # ---------- # Another group of parallel tests # ---------- -test: privileges security_label collate +test: privileges rowlevelsec security_label collate test: misc # rules cannot run concurrently with any test that creates a view diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule index 0bc5df7..290b24b 100644 --- a/src/test/regress/serial_schedule +++ b/src/test/regress/serial_schedule @@ -92,6 +92,7 @@ test: delete test: namespace test: prepared_xacts test: privileges +test: rowlevelsec test: security_label test: collate test: misc diff --git a/src/test/regress/sql/rowlevelsec.sql b/src/test/regress/sql/rowlevelsec.sql new file mode 100644 index 0000000..a4d1ccd --- /dev/null +++ b/src/test/regress/sql/rowlevelsec.sql @@ -0,0 +1,237 @@ +-- +-- Test of Row-level security feature +-- + +-- Clean up in case a prior regression run failed + +-- Suppress NOTICE messages when users/groups don't exist +SET client_min_messages TO 'warning'; + +DROP USER IF EXISTS rls_regress_user0; +DROP USER IF EXISTS rls_regress_user1; +DROP USER IF EXISTS rls_regress_user2; + +DROP SCHEMA IF EXISTS rls_regress_schema CASCADE; + +RESET client_min_messages; + +-- initial setup +CREATE USER rls_regress_user0; +CREATE USER rls_regress_user1; +CREATE USER rls_regress_user2; + +CREATE SCHEMA rls_regress_schema; +GRANT ALL ON SCHEMA rls_regress_schema TO public; +SET search_path = rls_regress_schema; + +CREATE TABLE account ( + pguser name primary key, + slevel int, + scategory bit(4) +); +GRANT SELECT,REFERENCES ON TABLE account TO public; +INSERT INTO account VALUES + ('rls_regress_user1', 1, B'0011'), + ('rls_regress_user2', 2, B'0110'), + ('rls_regress_user3', 0, B'0101'); + +CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool + COST 0.0000001 LANGUAGE plpgsql + AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END'; +GRANT EXECUTE ON FUNCTION f_leak(text) TO public; + +-- Creation of Test Data +SET SESSION AUTHORIZATION rls_regress_user0; + +CREATE TABLE document ( + did int primary key, + dlevel int, + dcategory bit(4), + dtitle text +); +GRANT ALL ON document TO public; +INSERT INTO document VALUES + ( 10, 0, B'0000', 'this document is unclassified, category(----)'), + ( 20, 0, B'0001', 'this document is unclassified, category(---A)'), + ( 30, 0, B'0010', 'this document is unclassified, category(--B-)'), + ( 40, 0, B'0011', 'this document is unclassified, category(--BA)'), + ( 50, 0, B'0100', 'this document is unclassified, category(-C--)'), + ( 60, 0, B'0101', 'this document is unclassified, category(-C-A)'), + ( 70, 0, B'0110', 'this document is unclassified, category(-CB-)'), + ( 80, 0, B'0111', 'this document is unclassified, category(-CBA)'), + ( 90, 1, B'0000', 'this document is classified, category(----)'), + (100, 1, B'0001', 'this document is classified, category(---A)'), + (110, 1, B'0010', 'this document is classified, category(--B-)'), + (120, 1, B'0011', 'this document is classified, category(--BA)'), + (130, 1, B'0100', 'this document is classified, category(-C--)'), + (140, 1, B'0101', 'this document is classified, category(-C-A)'), + (150, 1, B'0110', 'this document is classified, category(-CB-)'), + (160, 1, B'0111', 'this document is classified, category(-CBA)'), + (170, 2, B'0000', 'this document is secret, category(----)'), + (180, 2, B'0001', 'this document is secret, category(---A)'), + (190, 2, B'0010', 'this document is secret, category(--B-)'), + (200, 2, B'0011', 'this document is secret, category(--BA)'), + (210, 2, B'0100', 'this document is secret, category(-C--)'), + (220, 2, B'0101', 'this document is secret, category(-C-A)'), + (230, 2, B'0110', 'this document is secret, category(-CB-)'), + (240, 2, B'0111', 'this document is secret, category(-CBA)'); + +CREATE TABLE browse ( + pguser name references account(pguser), + did int references document(did), + ymd date +); +GRANT ALL ON browse TO public; +INSERT INTO browse VALUES + ('rls_regress_user1', 20, '2012-07-01'), + ('rls_regress_user1', 40, '2012-07-02'), + ('rls_regress_user1', 110, '2012-07-03'), + ('rls_regress_user2', 30, '2012-07-04'), + ('rls_regress_user2', 50, '2012-07-05'), + ('rls_regress_user2', 90, '2012-07-06'), + ('rls_regress_user2', 130, '2012-07-07'), + ('rls_regress_user2', 150, '2012-07-08'), + ('rls_regress_user2', 150, '2012-07-08'), + ('rls_regress_user2', 190, '2012-07-09'), + ('rls_regress_user2', 210, '2012-07-10'), + ('rls_regress_user3', 10, '2012-07-11'), + ('rls_regress_user3', 50, '2012-07-12'); + +-- user's sensitivity level must higher than document's level +ALTER TABLE document SET ROW LEVEL SECURITY + (dlevel <= (SELECT slevel FROM account WHERE pguser = current_user)); +ALTER TABLE browse SET ROW LEVEL SECURITY + (pguser = current_user); + +-- viewpoint from rls_regress_user1 +SET SESSION AUTHORIZATION rls_regress_user1; +SELECT * FROM document WHERE f_leak(dtitle); +SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text); + +-- viewpoint from rls_regress_user2 +SET SESSION AUTHORIZATION rls_regress_user2; +SELECT * FROM document WHERE f_leak(dtitle); +SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text); + +EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle); +EXPLAIN SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text); + +-- change row-level security policy +ALTER TABLE document RESET ROW LEVEL SECURITY; -- failed +ALTER TABLE document SET ROW LEVEL SECURITY (true); -- failed + +SET SESSION AUTHORIZATION rls_regress_user0; +-- switch to category based control from level based +ALTER TABLE document SET ROW LEVEL SECURITY + (dcategory & (SELECT ~scategory FROM account WHERE pguser = current_user) = B'0000'); + +-- viewpoint from rls_regress_user1 again +SET SESSION AUTHORIZATION rls_regress_user1; +SELECT * FROM document WHERE f_leak(dtitle); +SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text); + +-- viewpoint from rls_regress_user2 again +SET SESSION AUTHORIZATION rls_regress_user2; +SELECT * FROM document WHERE f_leak(dtitle); +SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text); + +EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle); +EXPLAIN SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text); + +-- Failed to update PK row referenced by invisible FK +SET SESSION AUTHORIZATION rls_regress_user1; +SELECT * FROM document NATURAL LEFT JOIN browse; +DELETE FROM document WHERE did = 30; -- failed +UPDATE document SET did = 9999 WHERE did = 90; -- failed + +-- +-- Table inheritance and RLS policy +-- +SET SESSION AUTHORIZATION rls_regress_user0; + +CREATE TABLE t1 (a int, b text, c text) WITH OIDS; +ALTER TABLE t1 DROP COLUMN b; -- just a disturbing factor +GRANT ALL ON t1 TO public; + +COPY t1 FROM stdin WITH (oids); +101 1 aaa +102 2 bbb +103 3 ccc +104 4 ddd +\. + +CREATE TABLE t2 (d float) INHERITS (t1); +COPY t2 FROM stdin WITH (oids); +201 1 abc 1.1 +202 2 bcd 2.2 +203 3 cde 3.3 +204 4 def 4.4 +\. + +CREATE TABLE t3 (e text) INHERITS (t1); +COPY t3 FROM stdin WITH (oids); +301 1 xxx X +302 2 yyy Y +303 3 zzz Z +\. + +ALTER TABLE t1 SET ROW LEVEL SECURITY (a % 2 = 0); -- be even number +ALTER TABLE t2 SET ROW LEVEL SECURITY (a % 2 = 1); -- be odd number + +SELECT * FROM t1; +EXPLAIN (costs off) SELECT * FROM t1; + +SELECT * FROM t1 WHERE f_leak(c); +EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(c); + +-- reference to system column +SELECT oid, * FROM t1; +EXPLAIN (costs off) SELECT * FROM t1; + +-- reference to whole-row reference +SELECT *,t1 FROM t1; +EXPLAIN (costs off) SELECT *,t1 FROM t1; + +-- for share/update lock +SELECT * FROM t1 FOR SHARE; +EXPLAIN (costs off) SELECT * FROM t1 FOR SHARE; + +SELECT * FROM t1 WHERE f_leak(c) FOR SHARE; +EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(c) FOR SHARE; + +-- Now COPY TO command does not support RLS +-- COPY t1 TO stdin; + +-- prepared statement with rls_regress_user0 privilege +PREPARE p1(int) AS SELECT * FROM t1 WHERE a <= $1; +EXECUTE p1(2); +EXPLAIN (costs off) EXECUTE p1(2); + +-- superuser is allowed to bypass RLS checks +RESET SESSION AUTHORIZATION; +SELECT * FROM t1 WHERE f_leak(c); +EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(c); + +-- plan cache should be invalidated +EXECUTE p1(2); +EXPLAIN (costs off) EXECUTE p1(2); + +PREPARE p2(int) AS SELECT * FROM t1 WHERE a = $1; +EXECUTE p2(2); +EXPLAIN (costs off) EXECUTE p2(2); + +-- also, case when privilege switch from superuser +SET SESSION AUTHORIZATION rls_regress_user0; +EXECUTE p2(2); +EXPLAIN (costs off) EXECUTE p2(2); + +-- +-- Clean up objects +-- +RESET SESSION AUTHORIZATION; + +DROP SCHEMA rls_regress_schema CASCADE; + +DROP USER rls_regress_user0; +DROP USER rls_regress_user1; +DROP USER rls_regress_user2;