doc/src/sgml/ref/alter_table.sgml | 38 ++ doc/src/sgml/user-manag.sgml | 125 +++++++ src/backend/catalog/Makefile | 2 +- src/backend/catalog/dependency.c | 22 ++ src/backend/commands/explain.c | 8 +- src/backend/commands/tablecmds.c | 185 ++++++++++ src/backend/optimizer/plan/planner.c | 7 + src/backend/optimizer/prep/preptlist.c | 54 ++- src/backend/optimizer/prep/prepunion.c | 28 +- src/backend/optimizer/util/Makefile | 2 +- src/backend/optimizer/util/rowlvsec.c | 524 ++++++++++++++++++++++++++++ src/backend/parser/gram.y | 16 + src/backend/utils/adt/acl.c | 34 ++ src/backend/utils/adt/ri_triggers.c | 33 +- src/backend/utils/cache/syscache.c | 12 + src/include/catalog/dependency.h | 1 + src/include/catalog/indexing.h | 3 + src/include/catalog/pg_proc.h | 7 + src/include/catalog/pg_rowlvsec.h | 39 +++ src/include/commands/tablecmds.h | 2 + src/include/nodes/parsenodes.h | 5 +- src/include/optimizer/rowlvsec.h | 31 ++ src/include/utils/syscache.h | 1 + src/test/regress/expected/rowlvsec.out | 438 +++++++++++++++++++++++ src/test/regress/expected/sanity_check.out | 3 +- src/test/regress/parallel_schedule | 2 +- src/test/regress/serial_schedule | 1 + src/test/regress/sql/rowlvsec.sql | 216 ++++++++++++ 28 files changed, 1821 insertions(+), 18 deletions(-) diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml index 04e3e54..a3c89d1 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..e4d5aaf 100644 --- a/doc/src/sgml/user-manag.sgml +++ b/doc/src/sgml/user-manag.sgml @@ -439,4 +439,129 @@ 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 are available to fetch, update + or delete, 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"())::text = uname) OR has_superuser_privilege()) +(4 rows) + + This query execution plan means the preconfigured row-level + security policy is implicitly added, and executed earlier than + user given conditions regardless to its cost. + On the other hand, these conditions are overwritten if current + user has superuser privilege. + + Similary, or + commands are also restricted with this policy, as follows. + +postgres=# EXPLAIN (costs off) UPDATE customer SET upasswd = md5('abcd') WHERE cid = 2; + QUERY PLAN +----------------------------------------------------------------------------------------- + Update on customer + -> Subquery Scan on customer + -> Index Scan using customer_pkey on customer + Index Cond: (cid = 2) + Filter: ((("current_user"())::text = uname) OR has_superuser_privilege()) +(5 rows) + + + + Please note that row-level security policy of the parent + table is not applied to child relations. Scope of 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) OR has_superuser_privilege()) + -> Seq Scan on t2 t1 + Filter: f_leak(y) + -> Subquery Scan on t3 + Filter: f_leak(t3.y) + -> Seq Scan on t3 t1 + Filter: (((x % 2) = 1) OR has_superuser_privilege()) +(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 rows to be inserted + and newer revision of updated rows, thus, it is needed to + define before-row-insert or before-row-update trigger to check + whether the row's contents satisfies the policy. + + We cannot contain sub-query as a part of row-level security + policy, so access control decision has to be done with + information originated from the target relation. + + Row-level security policy implicitly added to reference for + the target relation may prevent query optimization, because + query optimization has to be done query execution of couse, + in spite of all the permission check shall be applied on + execution stage. + + 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/catalog/Makefile b/src/backend/catalog/Makefile index 62fc9b0..a21c116 100644 --- a/src/backend/catalog/Makefile +++ b/src/backend/catalog/Makefile @@ -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_rowlvsec.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 d4e1f76..73bb603 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_rowlvsec.h" #include "catalog/pg_tablespace.h" #include "catalog/pg_trigger.h" #include "catalog/pg_ts_config.h" @@ -60,6 +61,7 @@ #include "commands/proclang.h" #include "commands/schemacmds.h" #include "commands/seclabel.h" +#include "commands/tablecmds.h" #include "commands/tablespace.h" #include "commands/trigger.h" #include "commands/typecmds.h" @@ -1221,6 +1223,10 @@ doDeletion(const ObjectAddress *object, int flags) RemoveExtensionById(object->objectId); break; + case OCLASS_ROWLVSEC: + RemoveRowLvSecById(object->objectId); + break; + default: elog(ERROR, "unrecognized object class: %u", object->classId); @@ -2269,6 +2275,9 @@ getObjectClass(const ObjectAddress *object) case ExtensionRelationId: return OCLASS_EXTENSION; + + case RowLevelSecurityRelationId: + return OCLASS_ROWLVSEC; } /* shouldn't get here */ @@ -2903,6 +2912,19 @@ getObjectDescription(const ObjectAddress *object) break; } + case OCLASS_ROWLVSEC: + { + 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/commands/explain.c b/src/backend/commands/explain.c index 1e8f618..cdc7d3a 100644 --- a/src/backend/commands/explain.c +++ b/src/backend/commands/explain.c @@ -1825,8 +1825,12 @@ ExplainTargetRel(Plan *plan, Index rti, ExplainState *es) case T_TidScan: case T_ForeignScan: case T_ModifyTable: - /* Assert it's on a real relation */ - Assert(rte->rtekind == RTE_RELATION); + /* + * Assert it's on either a real relation, or a sub-query + * originated from a real relation with row-level security + */ + Assert(rte->rtekind == RTE_RELATION || + (rte->rtekind == RTE_SUBQUERY && OidIsValid(rte->relid))); objectname = get_rel_name(rte->relid); if (es->verbose) namespace = get_namespace_name(get_rel_namespace(rte->relid)); diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 5c69cfb..6c94ca2 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_rowlvsec.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 ATExecSetRowLvSecurity(Relation relation, Node *clause); static void ATExecGenericOptions(Relation rel, List *options); static void copy_relation_data(SMgrRelation rel, SMgrRelation dst, @@ -2834,6 +2836,8 @@ AlterTableGetLockLevel(List *cmds) case AT_SetTableSpace: /* must rewrite heap */ case AT_DropNotNull: /* may change some SQL plans */ case AT_SetNotNull: + case AT_SetRowLvSecurity: + case AT_ResetRowLvSecurity: case AT_GenericOptions: case AT_AlterColumnGenericOptions: cmd_lockmode = AccessExclusiveLock; @@ -3196,6 +3200,8 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd, case AT_DropInherit: /* NO INHERIT */ case AT_AddOf: /* OF */ case AT_DropOf: /* NOT OF */ + case AT_SetRowLvSecurity: + case AT_ResetRowLvSecurity: ATSimplePermissions(rel, ATT_TABLE); /* These commands never recurse */ /* No command-specific prep needed */ @@ -3471,6 +3477,10 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab, Relation rel, case AT_DropOf: ATExecDropOf(rel, lockmode); break; + case AT_SetRowLvSecurity: + case AT_ResetRowLvSecurity: + ATExecSetRowLvSecurity(rel, (Node *) cmd->def); + break; case AT_GenericOptions: ATExecGenericOptions(rel, (List *) cmd->def); break; @@ -7648,6 +7658,22 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, Assert(defaultexpr); break; + case OCLASS_ROWLVSEC: + /* + * 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: @@ -9745,6 +9771,165 @@ ATExecDropOf(Relation rel, LOCKMODE lockmode) heap_close(relationRelation, RowExclusiveLock); } + +/* + * ALTER TABLE SET ROW LEVEL SECURITY (...) OR + * RESET ROW LEVEL SECURITY + */ +static void +ATExecSetRowLvSecurity(Relation relation, Node *clause) +{ + Oid relationId = RelationGetRelid(relation); + Relation rls_rel; + + rls_rel = heap_open(RowLevelSecurityRelationId, RowExclusiveLock); + + if (clause) + { + /* SET ROW LEVEL SECURITY () */ + ParseState *pstate; + RangeTblEntry *rte; + Node *qual; + HeapTuple oldtup; + HeapTuple newtup; + Datum values[Natts_pg_rowlvsec]; + bool isnull[Natts_pg_rowlvsec]; + bool replace[Natts_pg_rowlvsec]; + ObjectAddress target; + ObjectAddress myself; + + memset(values, 0, sizeof(values)); + memset(isnull, 0, sizeof(isnull)); + memset(replace, 0, sizeof(replace)); + + /* 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"); + /* + * XXX - Likely, here is no technical reason why we don't support + * subLinks in the security policy. But, we don't implement the + * routines to support sub-queries in RLS policy. + */ + if (pstate->p_hasSubLinks) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot use subquery in 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"))); + + /* Do insert or update pg_rowlvsec catalog */ + oldtup = SearchSysCache1(RLSRELID, ObjectIdGetDatum(relationId)); + if (HeapTupleIsValid(oldtup)) + { + replace[Anum_pg_rowlvsec_rlsqual - 1] = true; + values[Anum_pg_rowlvsec_rlsqual - 1] + = CStringGetTextDatum(nodeToString(qual)); + + newtup = heap_modify_tuple(oldtup, + RelationGetDescr(rls_rel), + values, isnull, replace); + simple_heap_update(rls_rel, &newtup->t_self, newtup); + + deleteDependencyRecordsFor(RowLevelSecurityRelationId, + relationId, false); + ReleaseSysCache(oldtup); + } + else + { + values[Anum_pg_rowlvsec_rlsrelid - 1] + = ObjectIdGetDatum(relationId); + values[Anum_pg_rowlvsec_rlsqual - 1] + = CStringGetTextDatum(nodeToString(qual)); + + newtup = heap_form_tuple(RelationGetDescr(rls_rel), + values, isnull); + simple_heap_insert(rls_rel, newtup); + + } + CatalogUpdateIndexes(rls_rel, 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_NORMAL); + + recordDependencyOnExpr(&myself, qual, pstate->p_rtable, + DEPENDENCY_NORMAL); + + free_parsestate(pstate); + + CacheInvalidateRelcache(relation); + } + else + { + if (SearchSysCacheExists1(RLSRELID, ObjectIdGetDatum(relationId))) + { + ObjectAddress address; + + address.classId = RowLevelSecurityRelationId; + address.objectId = RelationGetRelid(relation); + address.objectSubId = 0; + + performDeletion(&address, DROP_RESTRICT, 0); + + CacheInvalidateRelcache(relation); + } + else + { + /* nothing to do here */ + elog(INFO, "relation %s has no row-level security policy, skipped", + RelationGetRelationName(relation)); + } + } + heap_close(rls_rel, RowExclusiveLock); +} + +/* + * Guts of Row-level security policy deletion. + */ +void +RemoveRowLvSecById(Oid relationId) +{ + Relation rls_rel; + HeapTuple tuple; + + rls_rel = heap_open(RowLevelSecurityRelationId, RowExclusiveLock); + + tuple = SearchSysCache1(RLSRELID, ObjectIdGetDatum(relationId)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for row-level security policy %u", + relationId); + + simple_heap_delete(rls_rel, &tuple->t_self); + + ReleaseSysCache(tuple); + + heap_close(rls_rel, RowExclusiveLock); +} + /* * ALTER FOREIGN TABLE OPTIONS (...) */ diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index df76341..f833e7f 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -388,6 +388,13 @@ subquery_planner(PlannerGlobal *glob, Query *parse, expand_inherited_tables(root); /* + * Apply row-level security policy of tables, if configured. + * It has to be processed after inherited tables are expanded, because + * row-level security policy has per-table basis. + */ + apply_rowlv_security(root); + + /* * Set hasHavingQual to remember if HAVING clause is present. Needed * because preprocess_expression will reduce a constant-true condition to * an empty qual list ... but "HAVING TRUE" is not a semantic no-op. diff --git a/src/backend/optimizer/prep/preptlist.c b/src/backend/optimizer/prep/preptlist.c index 1af4e7f..8c42617 100644 --- a/src/backend/optimizer/prep/preptlist.c +++ b/src/backend/optimizer/prep/preptlist.c @@ -38,6 +38,44 @@ static List *expand_targetlist(List *tlist, int command_type, Index result_relation, List *range_table); +/* + * lookup_varattno + * + * This routine returns an attribute number to reference a particular + * attribute. In case when the target relation is really relation, + * we can reference arbitrary attribute (including system column) + * without any translations. However, we have to translate varattno + * of Vat that references sub-queries being originated from regular + * relations with row-level security policy. + */ +static AttrNumber +lookup_varattno(AttrNumber attno, Index rt_index, List *rtables) +{ + RangeTblEntry *rte = rt_fetch(rt_index, rtables); + + if (rte->rtekind == RTE_SUBQUERY && + rte->subquery->querySource == QSRC_ROW_LEVEL_SECURITY) + { + ListCell *cell; + + foreach (cell, rte->subquery->targetList) + { + TargetEntry *tle = lfirst(cell); + Var *var; + + if (IsA(tle->expr, Const)) + continue; + + var = (Var *) tle->expr; + Assert(IsA(var, Var)); + + if (var->varattno == attno) + return tle->resno; + } + elog(ERROR, "invalid attno %d on the pseudo targetList", attno); + } + return attno; +} /* * preprocess_targetlist @@ -62,7 +100,9 @@ preprocess_targetlist(PlannerInfo *root, List *tlist) { RangeTblEntry *rte = rt_fetch(result_relation, range_table); - if (rte->subquery != NULL || rte->relid == InvalidOid) + if ((rte->subquery != NULL && + rte->subquery->querySource != QSRC_ROW_LEVEL_SECURITY) || + rte->relid == InvalidOid) elog(ERROR, "subquery cannot be result relation"); } @@ -95,7 +135,8 @@ preprocess_targetlist(PlannerInfo *root, List *tlist) { /* It's a regular table, so fetch its TID */ var = makeVar(rc->rti, - SelfItemPointerAttributeNumber, + lookup_varattno(SelfItemPointerAttributeNumber, + rc->rti, range_table), TIDOID, -1, InvalidOid, @@ -111,7 +152,8 @@ preprocess_targetlist(PlannerInfo *root, List *tlist) if (rc->isParent) { var = makeVar(rc->rti, - TableOidAttributeNumber, + lookup_varattno(TableOidAttributeNumber, + rc->rti, range_table), OIDOID, -1, InvalidOid, @@ -129,7 +171,7 @@ preprocess_targetlist(PlannerInfo *root, List *tlist) /* Not a table, so we need the whole row as a junk var */ var = makeWholeRowVar(rt_fetch(rc->rti, range_table), rc->rti, - 0, + lookup_varattno(0, rc->rti, range_table), false); snprintf(resname, sizeof(resname), "wholerow%u", rc->rowmarkId); tle = makeTargetEntry((Expr *) var, @@ -298,7 +340,9 @@ expand_targetlist(List *tlist, int command_type, if (!att_tup->attisdropped) { new_expr = (Node *) makeVar(result_relation, - attrno, + lookup_varattno(attrno, + result_relation, + range_table), atttype, atttypmod, attcollation, diff --git a/src/backend/optimizer/prep/prepunion.c b/src/backend/optimizer/prep/prepunion.c index 6475633..415e841 100644 --- a/src/backend/optimizer/prep/prepunion.c +++ b/src/backend/optimizer/prep/prepunion.c @@ -1696,7 +1696,33 @@ adjust_appendrel_attrs_mutator(Node *node, return (Node *) rowexpr; } } - /* system attributes don't need any other translation */ + else + { + RangeTblEntry *rte = rt_fetch(appinfo->child_relid, + context->root->parse->rtable); + if (rte->rtekind == RTE_SUBQUERY && + rte->subquery->querySource == QSRC_ROW_LEVEL_SECURITY) + { + ListCell *cell; + + foreach (cell, rte->subquery->targetList) + { + TargetEntry *subtle = lfirst(cell); + Var *subvar; + + if (IsA(subtle->expr, Const)) + continue; + + subvar = (Var *) subtle->expr; + if (var->varattno == subvar->varattno) + { + Assert(var->vartype == subvar->vartype); + Assert(var->vartypmod == subvar->vartypmod); + var->varattno = subtle->resno; + } + } + } + } } return (Node *) var; } diff --git a/src/backend/optimizer/util/Makefile b/src/backend/optimizer/util/Makefile index 3b2d16b..fe71c0c 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 rowlvsec.o tlist.o var.o include $(top_srcdir)/src/backend/common.mk diff --git a/src/backend/optimizer/util/rowlvsec.c b/src/backend/optimizer/util/rowlvsec.c new file mode 100644 index 0000000..d311bdd --- /dev/null +++ b/src/backend/optimizer/util/rowlvsec.c @@ -0,0 +1,524 @@ +/* + * 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/sysattr.h" +#include "catalog/pg_class.h" +#include "catalog/pg_rowlvsec.h" +#include "catalog/pg_type.h" +#include "nodes/makefuncs.h" +#include "nodes/nodes.h" +#include "nodes/nodeFuncs.h" +#include "nodes/primnodes.h" +#include "optimizer/rowlvsec.h" +#include "parser/parsetree.h" +#include "utils/builtins.h" +#include "utils/fmgroids.h" +#include "utils/lsyscache.h" +#include "utils/syscache.h" + +/* hook to allow extensions to assign their own security policy */ +rowlv_security_hook_type rowlv_security_hook = NULL; + +/* current working mode of row-level security policy */ +static RowLvSecMode rowlv_security_mode = RowLvSecEnabled; + +/* + * switch_rowlv_security_mode + * + * It switches the current performing mode of row-level security feature, + * and returns older mode. It enables to disable this functionality for + * temporary usage; such as foreign-key checks to prohibit update of PKs + * being referenced by others. + * The caller must ensure the saved older mode shall be restored, and + * it correctly works even if an error would be raised. A typical + * implementation encloses the code block with PG_TRY(), PG_CATCH() and + * PG_END_TRY() to catch errors and restore the saved mode prior to + * re-throw the error. + */ +RowLvSecMode +switch_rowlv_security_mode(RowLvSecMode new_mode) +{ + RowLvSecMode old_mode = rowlv_security_mode; + + rowlv_security_mode = new_mode; + + return old_mode; +} + +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; + + /* This variable is not reference the target sub-query */ + /* XXX - add comment here for inherited columns */ + + + /* + * 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); + Var *subvar; + + if (IsA(subtle->expr, Const)) + continue; + + subvar = (Var *)subtle->expr; + Assert(IsA(subvar, Var)); + + if (var->varattno == subvar->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); +} + +/* + * fixup_varattnos + * + * It fixes up varattno of Var node that referenced the relation with RLS + * policy but replaced to a sub-query. Here is no guarantee varattno fits + * with TargetEntry's resno of the sub-query, so needs to fix up them. + */ +static void +fixup_varattnos(PlannerInfo *root) +{ + fixup_var_context context; + ListCell *cell; + + /* + * 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); + + /* + * Fixup translated_vars of AppendRelInfo + */ + foreach (cell, root->append_rel_list) + { + AppendRelInfo *appinfo = lfirst(cell); + RangeTblEntry *prte; + RangeTblEntry *crte; + + prte = rt_fetch(appinfo->parent_relid, root->parse->rtable); + crte = rt_fetch(appinfo->child_relid, root->parse->rtable); + + /* + * Parent relation entry (that has rte->inh = true) is never + * expanded, so no need to adject order of translated_vars. + */ + Assert(!(prte->rtekind == RTE_SUBQUERY && + prte->subquery->querySource == QSRC_ROW_LEVEL_SECURITY)); + + /* + * In case when the child relation entry is a sub-query originated + * from a regular relation with RLS policy, attribute number to + * reference a particular column (especially, system column) was + * changed, thus, its correspondence relationship should be fixed. + */ + if (crte->rtekind == RTE_SUBQUERY && + crte->subquery->querySource == QSRC_ROW_LEVEL_SECURITY) + { + ListCell *l; + AttrNumber attnum = 1; + + /* Ensure to perform as a sub-query, not an inherited table */ + appinfo->child_reltype = InvalidOid; + + foreach (l, appinfo->translated_vars) + { + Var *var = lfirst(l); + + if (var != NULL) + { + ListCell *ll; + + Assert(IsA(var, Var)); + + foreach (ll, crte->subquery->targetList) + { + TargetEntry *subtle = lfirst(ll); + Var *subvar; + + /* skip references to dropped column */ + if (IsA(subtle->expr, Const)) + continue; + + subvar = (Var *)subtle->expr; + Assert(IsA(subvar, Var)); + /* + * The "resno" of TargetEntry that has a reference to + * the required column should be a corrent "varattno" + * of Var node that reference a column from outside of + * the sub-query. + */ + if (var->varattno == subvar->varattno) + { + var->varattno = subtle->resno; + break; + } + } + if (ll == NULL) + elog(ERROR, "invalid varattno %d", var->varattno); + } + attnum++; + } + } + } +} + +/* + * make_pseudo_column + * + * It is a utility routine to generate a TargetEntry object that references + * a particular attribute of underlying relation. Its "expr" field shall + * have either Var or Const node. In case when the supplied "attnum" would + * be already dropped column, it injects a NULL value for a placeholder, + * according to the manner in preprocess_targetlist(). + */ +static TargetEntry * +make_pseudo_column(RangeTblEntry *subrte, AttrNumber attnum) +{ + HeapTuple atttup; + Form_pg_attribute attform; + Expr *expr; + char *resname; + + Assert(attnum != InvalidAttrNumber); + + atttup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(subrte->relid), + Int16GetDatum(attnum)); + if (!HeapTupleIsValid(atttup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + attnum, subrte->relid); + attform = (Form_pg_attribute) GETSTRUCT(atttup); + + if (attform->attisdropped) + { + /* Insert NULL just for a placeholder for dropped column */ + expr = (Expr *) makeConst(INT4OID, + -1, + InvalidOid, + sizeof(int32), + (Datum) 0, + true, /* isnull */ + true /* byval */ ); + } + else + { + expr = (Expr *) makeVar((Index) 1, + attform->attnum, + attform->atttypid, + attform->atttypmod, + InvalidOid, + 0); + } + resname = pstrdup(NameStr(attform->attname)); + + ReleaseSysCache(atttup); + + return makeTargetEntry(expr, -1, resname, false); +} + +/* + * pull_rowlv_security_policy + * + * This routine pulls an expression node from pg_rowlvsec system + * catalog, if configured. In addition to the configured qualifier, + * it also allows extensions to append its own row-level policy. + */ +static Expr * +pull_rowlv_security_policy(PlannerInfo *root, RangeTblEntry *rte) +{ + HeapTuple tuple; + Expr *qual_expr = NULL; + + Assert(rte->rtekind == RTE_RELATION && OidIsValid(rte->relid)); + + tuple = SearchSysCache1(RLSRELID, ObjectIdGetDatum(rte->relid)); + if (HeapTupleIsValid(tuple)) + { + Datum datum; + bool isnull; + char *sepol_string; + Expr *super_expr; + Expr *sepol_expr; + + datum = SysCacheGetAttr(RLSRELID, tuple, + Anum_pg_rowlvsec_rlsqual, &isnull); + Assert(!isnull); + + /* + * XXX - Row-level security policy has the following form: + * (( user-supplied-condition ) OR has_superuser_privilege()) + * that allows superusers to bypass all the controls, and make + * sure complete backup. + */ + sepol_string = TextDatumGetCString(datum); + sepol_expr = (Expr *) stringToNode(sepol_string); + Assert(exprType((Node *) sepol_expr) == BOOLOID); + + super_expr = (Expr *) makeFuncExpr(F_HAS_SUPERUSER_PRIVILEGE, + BOOLOID, NIL, + InvalidOid, + InvalidOid, + COERCE_DONTCARE); + qual_expr = makeBoolExpr(OR_EXPR, + list_make2(sepol_expr, + super_expr), -1); + pfree(sepol_string); + + ReleaseSysCache(tuple); + } + + /* + * This hook allows extensitons to append its own row-level security + * policy function. If qual_expr is not NULL, it should be connected + * with AND operator. + */ + if (rowlv_security_hook) + qual_expr = (*rowlv_security_hook)(root, rte, qual_expr); + + return qual_expr; +} + +/* + * expand_rtentry_with_policy + * + * It replaces a particular RangeTblEntry to the relation with RLS policy + * by a simple sub-query that references the relation with WHERE clause + * to perform as row-level security policy. + */ +static void +expand_rtentry_with_policy(PlannerInfo *root, + RangeTblEntry *rte, + Expr *qual_expr) +{ + Query *subqry; + RangeTblEntry *subrte; + RangeTblRef *subrtr; + TargetEntry *subtle; + HeapTuple reltup; + Form_pg_class relform; + AttrNumber attno; + List *targetList = NIL; + List *colNameList = NIL; + + Assert(rte->rtekind == RTE_RELATION && OidIsValid(rte->relid)); + + /* + * Construct a sub-query structure + */ + subqry = (Query *) makeNode(Query); + subqry->commandType = CMD_SELECT; + subqry->querySource = QSRC_ROW_LEVEL_SECURITY; + + subrte = copyObject(rte); + subrtr = makeNode(RangeTblRef); + subrtr->rtindex = 1; + + subqry->rtable = list_make1(subrte); + subqry->jointree = makeFromExpr(list_make1(subrtr), + (Node *) qual_expr); + + /* + * Construct pseudo columns as TargetEntry of sub-query that reference + * a particular attribute of the underlying relation. + */ + reltup = SearchSysCache1(RELOID, ObjectIdGetDatum(subrte->relid)); + if (!HeapTupleIsValid(reltup)) + elog(ERROR, "cache lookup failed for relation %u", subrte->relid); + relform = (Form_pg_class) GETSTRUCT(reltup); + + /* + * XXX - the pseudo columns that just references regular attribute of + * underlying table must has identical resno with attnum of the column, + * because preprocess_targetlist() sort-out target entries according to + * the attribute number to fit physical structure of the target relation + * in case when it has inherited tables. + * + * Likely, it can be optimized to have minimum number of TargetEntry + * except for a case of the target relation of UPDATE. But not yet. + */ + for (attno = 1; attno <= relform->relnatts; attno++) + { + subtle = make_pseudo_column(subrte, attno); + subtle->resno = list_length(targetList) + 1; + Assert(attno == subtle->resno); + targetList = lappend(targetList, subtle); + colNameList = lappend(colNameList, + makeString(pstrdup(subtle->resname))); + } + + /* + * Also, system columns and whole-row-reference are also added. + */ + for (attno = FirstLowInvalidHeapAttributeNumber + 1; attno < 0; attno++) + { + if (attno == ObjectIdAttributeNumber && !relform->relhasoids) + continue; + + subtle = make_pseudo_column(subrte, attno); + subtle->resno = list_length(targetList) + 1; + + targetList = lappend(targetList, subtle); + colNameList = lappend(colNameList, + makeString(pstrdup(subtle->resname))); + } + subqry->targetList = targetList; + ReleaseSysCache(reltup); + + /* + * Replace this RengeTblEntry by the sub-query with security-barrier + */ + rte->rtekind = RTE_SUBQUERY; + rte->subquery = subqry; + rte->security_barrier = true; + + /* no permission check on subquery itself */ + rte->requiredPerms = 0; + rte->checkAsUser = InvalidOid; + rte->selectedCols = NULL; + rte->modifiedCols = NULL; + + rte->alias = NULL; + rte->eref = makeAlias(get_rel_name(rte->relid), colNameList); +} + +/* + * apply_rowlv_security + * + * 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 +apply_rowlv_security(PlannerInfo *root) +{ + Query *parse = root->parse; + ListCell *cell; + Index rtindex; + bool needs_fixup = false; + + /* Mode checks */ + if (rowlv_security_mode == RowLvSecDisabled) + 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); + Expr *qual_expr; + + rtindex++; + + if (rte->rtekind != RTE_RELATION) + continue; + /* + * Parent relation of inheritance tree is just a placeholder here, + * so no need to apply row-level security them. + */ + if (rte->inh) + continue; + + /* + * It is unavailable to check rows to be inserted here, + * before-row-insert trigger should be utilized instead. + */ + if (parse->commandType == CMD_INSERT && + parse->resultRelation == rtindex) + continue; + + /* + * In case when row-level security policy was configured on the + * table referenced by this RangeTblEntry, it shall be rewritten + * to sub-query with the policy + */ + qual_expr = pull_rowlv_security_policy(root, rte); + if (qual_expr) + { + expand_rtentry_with_policy(root, rte, qual_expr); + needs_fixup = true; + } + } + + /* + * 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. + */ + if (needs_fixup) + fixup_varattnos(root); +} diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 9eb1bed..e9c8d5b 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 (clause) */ + | SET ROW LEVEL SECURITY '(' a_expr ')' + { + AlterTableCmd *n = makeNode(AlterTableCmd); + n->subtype = AT_SetRowLvSecurity; + n->def = (Node *) $6; + $$ = (Node *)n; + } + /* ALTER TABLE RESET ROW LEVEL SECURITY */ + | RESET ROW LEVEL SECURITY + { + AlterTableCmd *n = makeNode(AlterTableCmd); + n->subtype = AT_ResetRowLvSecurity; + n->def = NULL; + $$ = (Node *)n; + } | alter_generic_options { AlterTableCmd *n = makeNode(AlterTableCmd); diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c index 77322a1..bc34f1c 100644 --- a/src/backend/utils/adt/acl.c +++ b/src/backend/utils/adt/acl.c @@ -2863,6 +2863,40 @@ convert_column_priv_string(text *priv_type_text) return convert_any_priv_string(priv_type_text, column_priv_map); } +/* + * has_superuser_privilege + * checks superuser privilege of current user + */ +Datum +has_superuser_privilege(PG_FUNCTION_ARGS) +{ + PG_RETURN_BOOL(superuser()); +} + +/* + * has_superuser_privilege_name + * checks superuser privilege of the given username + */ +Datum +has_superuser_privilege_name(PG_FUNCTION_ARGS) +{ + Name username = PG_GETARG_NAME(0); + Oid roleid = get_role_oid_or_public(NameStr(*username)); + + PG_RETURN_BOOL(superuser_arg(roleid)); +} + +/* + * has_superuser_privilege_id + * checks superuser privilege of the given user-id + */ +Datum +has_superuser_privilege_id(PG_FUNCTION_ARGS) +{ + Oid roleid = PG_GETARG_OID(0); + + PG_RETURN_BOOL(superuser_arg(roleid)); +} /* * has_database_privilege variants diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c index dd58f4e..69fa60a 100644 --- a/src/backend/utils/adt/ri_triggers.c +++ b/src/backend/utils/adt/ri_triggers.c @@ -35,10 +35,12 @@ #include "catalog/pg_collation.h" #include "catalog/pg_constraint.h" #include "catalog/pg_operator.h" +#include "catalog/pg_rowlvsec.h" #include "catalog/pg_type.h" #include "commands/trigger.h" #include "executor/executor.h" #include "executor/spi.h" +#include "optimizer/rowlvsec.h" #include "parser/parse_coerce.h" #include "parser/parse_relation.h" #include "miscadmin.h" @@ -3341,6 +3343,7 @@ ri_PerformCheck(RI_QueryKey *qkey, SPIPlanPtr qplan, int spi_result; Oid save_userid; int save_sec_context; + RowLvSecMode save_rls_mode; Datum vals[RI_MAX_NUMKEYS * 2]; char nulls[RI_MAX_NUMKEYS * 2]; @@ -3424,11 +3427,31 @@ ri_PerformCheck(RI_QueryKey *qkey, SPIPlanPtr qplan, SetUserIdAndSecContext(RelationGetForm(query_rel)->relowner, save_sec_context | SECURITY_LOCAL_USERID_CHANGE); - /* Finally we can run the query. */ - spi_result = SPI_execute_snapshot(qplan, - vals, nulls, - test_snapshot, crosscheck_snapshot, - false, false, limit); + /* + * Switch row-level security model to prevent unexpected update or + * delete of PKs being referenced by invisible tuples in FK-side. + * However, we don't need to have special treatment towards INSERT + * or UPDATE FKs that tries to reference invisible PKs. + */ + save_rls_mode = switch_rowlv_security_mode(query_rel == fk_rel + ? RowLvSecDisabled + : RowLvSecEnabled); + PG_TRY(); + { + /* Finally we can run the query. */ + spi_result = SPI_execute_snapshot(qplan, + vals, nulls, + test_snapshot, + crosscheck_snapshot, + false, false, limit); + } + PG_CATCH(); + { + switch_rowlv_security_mode(save_rls_mode); + PG_RE_THROW(); + } + PG_END_TRY(); + switch_rowlv_security_mode(save_rls_mode); /* Restore UID and security context */ SetUserIdAndSecContext(save_userid, save_sec_context); diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c index c365ec7..f34bfa5 100644 --- a/src/backend/utils/cache/syscache.c +++ b/src/backend/utils/cache/syscache.c @@ -45,6 +45,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_range.h" #include "catalog/pg_rewrite.h" +#include "catalog/pg_rowlvsec.h" #include "catalog/pg_statistic.h" #include "catalog/pg_tablespace.h" #include "catalog/pg_ts_config.h" @@ -588,6 +589,17 @@ static const struct cachedesc cacheinfo[] = { }, 1024 }, + {RowLevelSecurityRelationId, /* RLSRELID */ + RowLevelSecurityIndexId, + 1, + { + Anum_pg_rowlvsec_rlsrelid, + 0, + 0, + 0 + }, + 128 + }, {RewriteRelationId, /* RULERELNAME */ RewriteRelRulenameIndexId, 2, diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h index f0eb564..37af71d 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_ROWLVSEC, /* pg_rowlvsec */ MAX_OCLASS /* MUST BE LAST */ } ObjectClass; diff --git a/src/include/catalog/indexing.h b/src/include/catalog/indexing.h index 450ec25..6c0ca5e 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_rowlvsec_relid_index, 3839, on pg_rowlvsec 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_proc.h b/src/include/catalog/pg_proc.h index 1e097dd..3d970fc 100644 --- a/src/include/catalog/pg_proc.h +++ b/src/include/catalog/pg_proc.h @@ -3279,6 +3279,13 @@ DESCR("(internal)"); DATA(insert OID = 2248 ( fmgr_sql_validator PGNSP PGUID 12 1 0 0 0 f f f f t f s 1 0 2278 "26" _null_ _null_ _null_ _null_ fmgr_sql_validator _null_ _null_ _null_ )); DESCR("(internal)"); +DATA(insert OID = 3819 ( has_superuser_privilege PGNSP PGUID 12 1 0 0 0 f f f f t f s 0 0 16 "" _null_ _null_ _null_ _null_ has_superuser_privilege _null_ _null_ _null_ )); +DESCR("superuser privilege of current user"); +DATA(insert OID = 3813 ( has_superuser_privilege PGNSP PGUID 12 1 0 0 0 f f f f t f s 1 0 16 "19" _null_ _null_ _null_ _null_ has_superuser_privilege_name _null_ _null_ _null_ )); +DESCR("superuser privilege by user name"); +DATA(insert OID = 3814 ( has_superuser_privilege PGNSP PGUID 12 1 0 0 0 f f f f t f s 1 0 16 "26" _null_ _null_ _null_ _null_ has_superuser_privilege_id _null_ _null_ _null_ )); +DESCR("superuser privilege by user oid"); + DATA(insert OID = 2250 ( has_database_privilege PGNSP PGUID 12 1 0 0 0 f f f f t f s 3 0 16 "19 25 25" _null_ _null_ _null_ _null_ has_database_privilege_name_name _null_ _null_ _null_ )); DESCR("user privilege on database by username, database name"); DATA(insert OID = 2251 ( has_database_privilege PGNSP PGUID 12 1 0 0 0 f f f f t f s 3 0 16 "19 26 25" _null_ _null_ _null_ _null_ has_database_privilege_name_id _null_ _null_ _null_ )); diff --git a/src/include/catalog/pg_rowlvsec.h b/src/include/catalog/pg_rowlvsec.h new file mode 100644 index 0000000..629c024 --- /dev/null +++ b/src/include/catalog/pg_rowlvsec.h @@ -0,0 +1,39 @@ +/* ------------------------------------------------------------------------- + * + * pg_rowlvsec.h + * definition of the system row-level security policy relation (pg_rowlvsec) + * + * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * ------------------------------------------------------------------------- + */ +#ifndef PG_ROWLVSEC_H +#define PG_ROWLVSEC_H + +#include "catalog/genbki.h" + +/* ---------------- + * pg_rowlvsec definition. cpp turns this into + * typedef struct FormData_pg_rowlvsec + * ---------------- + */ +#define RowLevelSecurityRelationId 3838 + +CATALOG(pg_rowlvsec,3838) BKI_WITHOUT_OIDS +{ + Oid rlsrelid; /* OID of the relation with RLS policy */ +#ifdef CATALOG_VARLEN + text rlsqual; /* Expression tree of the security policy */ +#endif +} Form_pg_rowlvsec; + +/* ---------------- + * compiler constants for pg_rowlvsec + * ---------------- + */ +#define Natts_pg_rowlvsec 2 +#define Anum_pg_rowlvsec_rlsrelid 1 +#define Anum_pg_rowlvsec_rlsqual 2 + +#endif /* PG_ROWLVSEC_H */ diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h index 9ceb086..66c02a9 100644 --- a/src/include/commands/tablecmds.h +++ b/src/include/commands/tablecmds.h @@ -40,6 +40,8 @@ extern void AlterRelationNamespaceInternal(Relation classRel, Oid relOid, Oid oldNspOid, Oid newNspOid, bool hasDependEntry); +extern void RemoveRowLvSecById(Oid relationId); + extern void CheckTableNotInUse(Relation rel, const char *stmt); extern void ExecuteTruncate(TruncateStmt *stmt); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index deff1a3..69726e2 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_SetRowLvSecurity, /* SET ROW LEVEL SECURITY (...) */ + AT_ResetRowLvSecurity, /* RESET ROW LEVEL SECURITY (...) */ AT_GenericOptions /* OPTIONS (...) */ } AlterTableType; diff --git a/src/include/optimizer/rowlvsec.h b/src/include/optimizer/rowlvsec.h new file mode 100644 index 0000000..92b912f --- /dev/null +++ b/src/include/optimizer/rowlvsec.h @@ -0,0 +1,31 @@ +/* ------------------------------------------------------------------------- + * + * rowlvsec.h + * prototypes for rowlvsec.c + * + * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * ------------------------------------------------------------------------- + */ +#ifndef ROWLVSEC_H +#define ROWLVSEC_H + +#include "nodes/relation.h" + +typedef Expr *(*rowlv_security_hook_type)(PlannerInfo *root, + RangeTblEntry *rte, + Expr *qual_expr); +extern PGDLLIMPORT rowlv_security_hook_type rowlv_security_hook; + +typedef enum +{ + RowLvSecEnabled, + RowLvSecDisabled, +} RowLvSecMode; + +extern RowLvSecMode switch_rowlv_security_mode(RowLvSecMode new_mode); + +extern void apply_rowlv_security(PlannerInfo *root); + +#endif /* ROWLVSEC_H */ diff --git a/src/include/utils/syscache.h b/src/include/utils/syscache.h index d59dd4e..4caf971 100644 --- a/src/include/utils/syscache.h +++ b/src/include/utils/syscache.h @@ -73,6 +73,7 @@ enum SysCacheIdentifier RANGETYPE, RELNAMENSP, RELOID, + RLSRELID, RULERELNAME, STATRELATTINH, TABLESPACEOID, diff --git a/src/test/regress/expected/rowlvsec.out b/src/test/regress/expected/rowlvsec.out new file mode 100644 index 0000000..9ae7454 --- /dev/null +++ b/src/test/regress/expected/rowlvsec.out @@ -0,0 +1,438 @@ +-- +-- Test 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 rowlvsec_user1; +DROP USER IF EXISTS rowlvsec_user2; +DROP USER IF EXISTS rowlvsec_user3; +DROP SCHEMA IF EXISTS rowlvsec_schema CASCADE; +RESET client_min_messages; +-- Initial Setup +CREATE USER rowlvsec_user0; +CREATE USER rowlvsec_user1; +CREATE USER rowlvsec_user2; +CREATE USER rowlvsec_user3; +CREATE SCHEMA rowlvsec_schema; +GRANT ALL ON SCHEMA rowlvsec_schema TO public; +SET search_path = rowlvsec_schema,public; +CREATE TABLE customer ( + cid name primary key, + cname text, + cage int, + caddr text +); +NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "customer_pkey" for table "customer" +GRANT SELECT ON TABLE customer TO public; +INSERT INTO customer VALUES + ('rowlvsec_user1', 'alice', 18, 'USA'), + ('rowlvsec_user2', 'bob', 24, 'Japan'), + ('rowlvsec_user3', 'carol', 29, 'USA'), + ('rowlvsec_user4', 'dave', 32, 'Germany'); +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; +-- XXX sub-query in RLS policy should be supported in the future +CREATE FUNCTION customer_age() RETURNS int STABLE LANGUAGE sql + AS 'SELECT cage FROM customer WHERE cid = session_user'; +GRANT EXECUTE ON FUNCTION customer_age() TO public; +CREATE FUNCTION customer_name() RETURNS text STABLE LANGUAGE sql + AS 'SELECT cname FROM customer WHERE cid = session_user'; +GRANT EXECUTE ON FUNCTION customer_age() TO public; +CREATE FUNCTION customer_addr() RETURNS text STABLE LANGUAGE sql + AS 'SELECT caddr FROM customer WHERE cid = session_user'; +GRANT EXECUTE ON FUNCTION customer_addr() TO public; +-- Create Test Data +SET SESSION AUTHORIZATION rowlvsec_user0; +CREATE TABLE drink ( + id int primary key, + name text, + price int, + alcohol bool, + madein text +); +NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "drink_pkey" for table "drink" +GRANT ALL ON drink TO public; +INSERT INTO drink VALUES + (10, 'water', 100, false, 'Japan'), + (20, 'juice', 180, false, 'Germany'), + (30, 'coke', 120, false, 'USA'), + (40, 'coffee', 200, false, 'USA'), + (50, 'beer', 280, true, 'Germany'), + (60, 'wine', 340, true, 'USA'), + (70, 'sake', 300, true, 'Japan'); +CREATE TABLE purchase ( + id int references drink(id), + cname text default customer_name(), + ymd date +); +GRANT ALL ON purchase TO public; +INSERT INTO purchase VALUES + (10, 'alice', '2012-05-03'), + (10, 'bob', '2012-05-04'), + (10, 'alice', '2012-05-08'), + (20, 'alice', '2012-05-15'), + (20, 'bob', '2012-05-18'), + (20, 'carol', '2012-05-21'), + (30, 'alice', '2012-05-28'), + (40, 'bob', '2012-06-02'), + (60, 'carol', '2012-06-10'); +ALTER TABLE drink SET ROW LEVEL SECURITY (customer_age() > 19 OR NOT alcohol); +ALTER TABLE purchase SET ROW LEVEL SECURITY (cname = customer_name()); +-- Viewpoint from Alice +SET SESSION AUTHORIZATION rowlvsec_user1; +SELECT * FROM drink WHERE f_leak(name); +NOTICE: f_leak => water +NOTICE: f_leak => juice +NOTICE: f_leak => coke +NOTICE: f_leak => coffee + id | name | price | alcohol | madein +----+--------+-------+---------+--------- + 10 | water | 100 | f | Japan + 20 | juice | 180 | f | Germany + 30 | coke | 120 | f | USA + 40 | coffee | 200 | f | USA +(4 rows) + +SELECT * FROM purchase; + id | cname | ymd +----+-------+------------ + 10 | alice | 05-03-2012 + 10 | alice | 05-08-2012 + 20 | alice | 05-15-2012 + 30 | alice | 05-28-2012 +(4 rows) + +SELECT * FROM drink NATURAL JOIN purchase; + id | name | price | alcohol | madein | cname | ymd +----+-------+-------+---------+---------+-------+------------ + 10 | water | 100 | f | Japan | alice | 05-03-2012 + 10 | water | 100 | f | Japan | alice | 05-08-2012 + 20 | juice | 180 | f | Germany | alice | 05-15-2012 + 30 | coke | 120 | f | USA | alice | 05-28-2012 +(4 rows) + +-- Viewpoint from Bob +SET SESSION AUTHORIZATION rowlvsec_user2; +SELECT * FROM drink; + id | name | price | alcohol | madein +----+--------+-------+---------+--------- + 10 | water | 100 | f | Japan + 20 | juice | 180 | f | Germany + 30 | coke | 120 | f | USA + 40 | coffee | 200 | f | USA + 50 | beer | 280 | t | Germany + 60 | wine | 340 | t | USA + 70 | sake | 300 | t | Japan +(7 rows) + +SELECT * FROM purchase WHERE f_leak(cname); +NOTICE: f_leak => bob +NOTICE: f_leak => bob +NOTICE: f_leak => bob + id | cname | ymd +----+-------+------------ + 10 | bob | 05-04-2012 + 20 | bob | 05-18-2012 + 40 | bob | 06-02-2012 +(3 rows) + +SELECT * FROM drink NATURAL JOIN purchase; + id | name | price | alcohol | madein | cname | ymd +----+--------+-------+---------+---------+-------+------------ + 10 | water | 100 | f | Japan | bob | 05-04-2012 + 20 | juice | 180 | f | Germany | bob | 05-18-2012 + 40 | coffee | 200 | f | USA | bob | 06-02-2012 +(3 rows) + +EXPLAIN (costs off) SELECT * FROM drink WHERE f_leak(name); + QUERY PLAN +--------------------------------------------------------------------------------------- + Subquery Scan on drink + Filter: f_leak(drink.name) + -> Seq Scan on drink + Filter: ((customer_age() > 19) OR (NOT alcohol) OR has_superuser_privilege()) +(4 rows) + +EXPLAIN (costs off) SELECT * FROM purchase WHERE f_leak(cname); + QUERY PLAN +-------------------------------------------------------------------------- + Subquery Scan on purchase + Filter: f_leak(purchase.cname) + -> Seq Scan on purchase + Filter: ((cname = customer_name()) OR has_superuser_privilege()) +(4 rows) + +EXPLAIN (costs off) SELECT * FROM drink NATURAL JOIN purchase; + QUERY PLAN +--------------------------------------------------------------------------------------------------- + Hash Join + Hash Cond: (rowlvsec_schema.purchase.id = drink.id) + -> Seq Scan on purchase + Filter: ((cname = customer_name()) OR has_superuser_privilege()) + -> Hash + -> Subquery Scan on drink + -> Seq Scan on drink + Filter: ((customer_age() > 19) OR (NOT alcohol) OR has_superuser_privilege()) +(8 rows) + +ALTER TABLE drink RESET ROW LEVEL SECURITY; -- failed +ERROR: must be owner of relation drink +ALTER TABLE drink SET ROW LEVEL SECURITY (customer_name() = 'bob'); -- failed +ERROR: must be owner of relation drink +-- Change the security policy +SET SESSION AUTHORIZATION rowlvsec_user0; +ALTER TABLE drink SET ROW LEVEL SECURITY (madein = customer_addr()); +-- Viewpoint from Alice again +SET SESSION AUTHORIZATION rowlvsec_user1; +SELECT * FROM drink WHERE id > 30; + id | name | price | alcohol | madein +----+--------+-------+---------+-------- + 40 | coffee | 200 | f | USA + 60 | wine | 340 | t | USA +(2 rows) + +-- Viewpoint from Bob again +SET SESSION AUTHORIZATION rowlvsec_user2; +SELECT * FROM drink WHERE f_leak(name); +NOTICE: f_leak => water +NOTICE: f_leak => sake + id | name | price | alcohol | madein +----+-------+-------+---------+-------- + 10 | water | 100 | f | Japan + 70 | sake | 300 | t | Japan +(2 rows) + +-- Writer Access and PK&FK constraints +SET SESSION AUTHORIZATION rowlvsec_user1; +SELECT * FROM drink NATURAL FULL OUTER JOIN purchase; + id | name | price | alcohol | madein | cname | ymd +----+--------+-------+---------+--------+-------+------------ + 10 | | | | | alice | 05-03-2012 + 10 | | | | | alice | 05-08-2012 + 20 | | | | | alice | 05-15-2012 + 30 | coke | 120 | f | USA | alice | 05-28-2012 + 40 | coffee | 200 | f | USA | | + 60 | wine | 340 | t | USA | | +(6 rows) + +BEGIN; +UPDATE purchase SET ymd = ymd + 1 RETURNING *; + id | cname | ymd +----+-------+------------ + 10 | alice | 05-04-2012 + 10 | alice | 05-09-2012 + 20 | alice | 05-16-2012 + 30 | alice | 05-29-2012 +(4 rows) + +EXPLAIN (costs off) UPDATE purchase SET ymd = ymd + 1; + QUERY PLAN +-------------------------------------------------------------------------------- + Update on purchase + -> Subquery Scan on purchase + -> Seq Scan on purchase + Filter: ((cname = customer_name()) OR has_superuser_privilege()) +(4 rows) + +ABORT; +BEGIN; +DELETE FROM purchase RETURNING *; + id | cname | ymd +----+-------+------------ + 10 | alice | 05-03-2012 + 10 | alice | 05-08-2012 + 20 | alice | 05-15-2012 + 30 | alice | 05-28-2012 +(4 rows) + +EXPLAIN (costs off) DELETE FROM purchase; + QUERY PLAN +-------------------------------------------------------------------------------- + Delete on purchase + -> Subquery Scan on purchase + -> Seq Scan on purchase + Filter: ((cname = customer_name()) OR has_superuser_privilege()) +(4 rows) + +ABORT; +-- Failed to update PK row referenced by invisible FK +UPDATE drink SET id = 100 WHERE id = 60; -- fail +ERROR: update or delete on table "drink" violates foreign key constraint "purchase_id_fkey" on table "purchase" +DETAIL: Key (id)=(60) is still referenced from table "purchase". +DELETE FROM drink WHERE id = 40; -- fail +ERROR: update or delete on table "drink" violates foreign key constraint "purchase_id_fkey" on table "purchase" +DETAIL: Key (id)=(40) is still referenced from table "purchase". +-- Failed to insert FK to reference invisible PK +INSERT INTO purchase (id, ymd) VALUES (20, '2012-06-12'); -- fail +ERROR: insert or update on table "purchase" violates foreign key constraint "purchase_id_fkey" +DETAIL: Key (id)=(20) is not present in table "drink". +INSERT INTO purchase (id, ymd) VALUES (30, '2012-06-12'); -- OK +-- Same writter access from Bob +SET SESSION AUTHORIZATION rowlvsec_user2; +SELECT * FROM drink NATURAL FULL OUTER JOIN purchase; + id | name | price | alcohol | madein | cname | ymd +----+-------+-------+---------+--------+-------+------------ + 10 | water | 100 | f | Japan | bob | 05-04-2012 + 20 | | | | | bob | 05-18-2012 + 40 | | | | | bob | 06-02-2012 + 70 | sake | 300 | t | Japan | | +(4 rows) + +BEGIN; +UPDATE purchase SET ymd = ymd + 1 RETURNING *; + id | cname | ymd +----+-------+------------ + 10 | bob | 05-05-2012 + 20 | bob | 05-19-2012 + 40 | bob | 06-03-2012 +(3 rows) + +ABORT; +BEGIN; +DELETE FROM purchase RETURNING *; + id | cname | ymd +----+-------+------------ + 10 | bob | 05-04-2012 + 20 | bob | 05-18-2012 + 40 | bob | 06-02-2012 +(3 rows) + +ABORT; +-- Table inheritance and RLS policy +SET SESSION AUTHORIZATION rowlvsec_user0; +CREATE TABLE t1 (a int, b text, c text); +ALTER TABLE t1 DROP COLUMN b; -- just a disturbing factor +GRANT ALL ON t1 TO public; +INSERT INTO t1 VALUES + (1,'aaa'), (2,'bbb'), (3,'ccc'), (4,'ddd'), ( 5,'eee'), + (6,'fff'), (7,'ggg'), (8,'hhh'), (9,'iii'), (10,'jjj'); +CREATE TABLE t2 (x int) INHERITS (t1); +INSERT INTO t2 VALUES + (1,'abc'), (2,'bcd'), (3,'cde'), (4,'def'), ( 5,'efg'), + (6,'fgh'), (7,'ghi'), (8,'hij'), (9,'ijk'), (10,'jkl'); +CREATE TABLE t3 (y int) INHERITS (t1); +INSERT INTO t3 VALUES (1,'xxx'),(2,'yyy'),(3,'zzz'),(4,'xyz'); +ALTER TABLE t1 SET ROW LEVEL SECURITY (a % 4 = customer_age() % 4); +ALTER TABLE t2 SET ROW LEVEL SECURITY ((a+2) % 4 = customer_age() % 4); +-- viewpoint from Alice +SET SESSION AUTHORIZATION rowlvsec_user1; +SELECT * FROM t1; + a | c +----+----- + 2 | bbb + 6 | fff + 10 | jjj + 4 | def + 8 | hij + 1 | xxx + 2 | yyy + 3 | zzz + 4 | xyz +(9 rows) + +EXPLAIN (costs off) SELECT * FROM t1; + QUERY PLAN +--------------------------------------------------------------------------------------------------- + Result + -> Append + -> Subquery Scan on t1 + -> Seq Scan on t1 + Filter: (((a % 4) = (customer_age() % 4)) OR has_superuser_privilege()) + -> Subquery Scan on t2 + -> Seq Scan on t2 t1 + Filter: ((((a + 2) % 4) = (customer_age() % 4)) OR has_superuser_privilege()) + -> Seq Scan on t3 t1 +(9 rows) + +BEGIN; +UPDATE t1 SET c = c || '_updt'; +EXPLAIN (costs off) UPDATE t1 SET c = c || '_updt'; + QUERY PLAN +--------------------------------------------------------------------------------------------- + Update on t1 + -> Subquery Scan on t1 + -> Seq Scan on t1 + Filter: (((a % 4) = (customer_age() % 4)) OR has_superuser_privilege()) + -> Subquery Scan on t2 + -> Seq Scan on t2 t1 + Filter: ((((a + 2) % 4) = (customer_age() % 4)) OR has_superuser_privilege()) + -> Seq Scan on t3 t1 +(8 rows) + +ABORT; +BEGIN; +DELETE FROM t1; +EXPLAIN (costs off) DELETE FROM t1; + QUERY PLAN +--------------------------------------------------------------------------------------------- + Delete on t1 + -> Subquery Scan on t1 + -> Seq Scan on t1 + Filter: (((a % 4) = (customer_age() % 4)) OR has_superuser_privilege()) + -> Subquery Scan on t2 + -> Seq Scan on t2 t1 + Filter: ((((a + 2) % 4) = (customer_age() % 4)) OR has_superuser_privilege()) + -> Seq Scan on t3 t1 +(8 rows) + +ABORT; +-- viewpoint from Carol +SET SESSION AUTHORIZATION rowlvsec_user3; +SELECT * FROM t1; + a | c +---+----- + 1 | aaa + 5 | eee + 9 | iii + 3 | cde + 7 | ghi + 1 | xxx + 2 | yyy + 3 | zzz + 4 | xyz +(9 rows) + +EXPLAIN (costs off) SELECT * FROM t1; + QUERY PLAN +--------------------------------------------------------------------------------------------------- + Result + -> Append + -> Subquery Scan on t1 + -> Seq Scan on t1 + Filter: (((a % 4) = (customer_age() % 4)) OR has_superuser_privilege()) + -> Subquery Scan on t2 + -> Seq Scan on t2 t1 + Filter: ((((a + 2) % 4) = (customer_age() % 4)) OR has_superuser_privilege()) + -> Seq Scan on t3 t1 +(9 rows) + +BEGIN; +UPDATE t1 SET c = c || '_updt'; +ABORT; +BEGIN; +DELETE FROM t1; +ABORT; +-- Clean up objects +RESET SESSION AUTHORIZATION; +DROP SCHEMA rowlvsec_schema CASCADE; +NOTICE: drop cascades to 14 other objects +DETAIL: drop cascades to table customer +drop cascades to function f_leak(text) +drop cascades to function customer_age() +drop cascades to function customer_name() +drop cascades to function customer_addr() +drop cascades to table drink +drop cascades to row-level security of drink +drop cascades to table purchase +drop cascades to row-level security of purchase +drop cascades to table t1 +drop cascades to row-level security of t1 +drop cascades to table t2 +drop cascades to row-level security of t2 +drop cascades to table t3 +DROP USER rowlvsec_user3; +DROP USER rowlvsec_user2; +DROP USER rowlvsec_user1; diff --git a/src/test/regress/expected/sanity_check.out b/src/test/regress/expected/sanity_check.out index 7f560d2..2b4932d 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_rowlvsec | 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..45a98c8 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 security_label collate rowlvsec 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..a35dace 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: rowlvsec test: security_label test: collate test: misc diff --git a/src/test/regress/sql/rowlvsec.sql b/src/test/regress/sql/rowlvsec.sql new file mode 100644 index 0000000..f7d72f6 --- /dev/null +++ b/src/test/regress/sql/rowlvsec.sql @@ -0,0 +1,216 @@ +-- +-- Test 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 rowlvsec_user1; +DROP USER IF EXISTS rowlvsec_user2; +DROP USER IF EXISTS rowlvsec_user3; + +DROP SCHEMA IF EXISTS rowlvsec_schema CASCADE; + +RESET client_min_messages; + +-- Initial Setup +CREATE USER rowlvsec_user0; +CREATE USER rowlvsec_user1; +CREATE USER rowlvsec_user2; +CREATE USER rowlvsec_user3; + +CREATE SCHEMA rowlvsec_schema; +GRANT ALL ON SCHEMA rowlvsec_schema TO public; +SET search_path = rowlvsec_schema,public; + +CREATE TABLE customer ( + cid name primary key, + cname text, + cage int, + caddr text +); +GRANT SELECT ON TABLE customer TO public; +INSERT INTO customer VALUES + ('rowlvsec_user1', 'alice', 18, 'USA'), + ('rowlvsec_user2', 'bob', 24, 'Japan'), + ('rowlvsec_user3', 'carol', 29, 'USA'), + ('rowlvsec_user4', 'dave', 32, 'Germany'); + +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; + +-- XXX sub-query in RLS policy should be supported in the future +CREATE FUNCTION customer_age() RETURNS int STABLE LANGUAGE sql + AS 'SELECT cage FROM customer WHERE cid = session_user'; +GRANT EXECUTE ON FUNCTION customer_age() TO public; + +CREATE FUNCTION customer_name() RETURNS text STABLE LANGUAGE sql + AS 'SELECT cname FROM customer WHERE cid = session_user'; +GRANT EXECUTE ON FUNCTION customer_age() TO public; + +CREATE FUNCTION customer_addr() RETURNS text STABLE LANGUAGE sql + AS 'SELECT caddr FROM customer WHERE cid = session_user'; +GRANT EXECUTE ON FUNCTION customer_addr() TO public; + +-- Create Test Data +SET SESSION AUTHORIZATION rowlvsec_user0; + +CREATE TABLE drink ( + id int primary key, + name text, + price int, + alcohol bool, + madein text +); +GRANT ALL ON drink TO public; +INSERT INTO drink VALUES + (10, 'water', 100, false, 'Japan'), + (20, 'juice', 180, false, 'Germany'), + (30, 'coke', 120, false, 'USA'), + (40, 'coffee', 200, false, 'USA'), + (50, 'beer', 280, true, 'Germany'), + (60, 'wine', 340, true, 'USA'), + (70, 'sake', 300, true, 'Japan'); + +CREATE TABLE purchase ( + id int references drink(id), + cname text default customer_name(), + ymd date +); +GRANT ALL ON purchase TO public; +INSERT INTO purchase VALUES + (10, 'alice', '2012-05-03'), + (10, 'bob', '2012-05-04'), + (10, 'alice', '2012-05-08'), + (20, 'alice', '2012-05-15'), + (20, 'bob', '2012-05-18'), + (20, 'carol', '2012-05-21'), + (30, 'alice', '2012-05-28'), + (40, 'bob', '2012-06-02'), + (60, 'carol', '2012-06-10'); + +ALTER TABLE drink SET ROW LEVEL SECURITY (customer_age() > 19 OR NOT alcohol); +ALTER TABLE purchase SET ROW LEVEL SECURITY (cname = customer_name()); + +-- Viewpoint from Alice +SET SESSION AUTHORIZATION rowlvsec_user1; +SELECT * FROM drink WHERE f_leak(name); +SELECT * FROM purchase; +SELECT * FROM drink NATURAL JOIN purchase; + +-- Viewpoint from Bob +SET SESSION AUTHORIZATION rowlvsec_user2; +SELECT * FROM drink; +SELECT * FROM purchase WHERE f_leak(cname); +SELECT * FROM drink NATURAL JOIN purchase; + +EXPLAIN (costs off) SELECT * FROM drink WHERE f_leak(name); +EXPLAIN (costs off) SELECT * FROM purchase WHERE f_leak(cname); +EXPLAIN (costs off) SELECT * FROM drink NATURAL JOIN purchase; + +ALTER TABLE drink RESET ROW LEVEL SECURITY; -- failed +ALTER TABLE drink SET ROW LEVEL SECURITY (customer_name() = 'bob'); -- failed + +-- Change the security policy +SET SESSION AUTHORIZATION rowlvsec_user0; +ALTER TABLE drink SET ROW LEVEL SECURITY (madein = customer_addr()); + +-- Viewpoint from Alice again +SET SESSION AUTHORIZATION rowlvsec_user1; +SELECT * FROM drink WHERE id > 30; + +-- Viewpoint from Bob again +SET SESSION AUTHORIZATION rowlvsec_user2; +SELECT * FROM drink WHERE f_leak(name); + +-- Writer Access and PK&FK constraints +SET SESSION AUTHORIZATION rowlvsec_user1; + +SELECT * FROM drink NATURAL FULL OUTER JOIN purchase; + +BEGIN; +UPDATE purchase SET ymd = ymd + 1 RETURNING *; +EXPLAIN (costs off) UPDATE purchase SET ymd = ymd + 1; +ABORT; + +BEGIN; +DELETE FROM purchase RETURNING *; +EXPLAIN (costs off) DELETE FROM purchase; +ABORT; + +-- Failed to update PK row referenced by invisible FK +UPDATE drink SET id = 100 WHERE id = 60; -- fail +DELETE FROM drink WHERE id = 40; -- fail + +-- Failed to insert FK to reference invisible PK +INSERT INTO purchase (id, ymd) VALUES (20, '2012-06-12'); -- fail +INSERT INTO purchase (id, ymd) VALUES (30, '2012-06-12'); -- OK + +-- Same writter access from Bob +SET SESSION AUTHORIZATION rowlvsec_user2; +SELECT * FROM drink NATURAL FULL OUTER JOIN purchase; + +BEGIN; +UPDATE purchase SET ymd = ymd + 1 RETURNING *; +ABORT; + +BEGIN; +DELETE FROM purchase RETURNING *; +ABORT; + +-- Table inheritance and RLS policy +SET SESSION AUTHORIZATION rowlvsec_user0; + +CREATE TABLE t1 (a int, b text, c text); +ALTER TABLE t1 DROP COLUMN b; -- just a disturbing factor +GRANT ALL ON t1 TO public; + +INSERT INTO t1 VALUES + (1,'aaa'), (2,'bbb'), (3,'ccc'), (4,'ddd'), ( 5,'eee'), + (6,'fff'), (7,'ggg'), (8,'hhh'), (9,'iii'), (10,'jjj'); +CREATE TABLE t2 (x int) INHERITS (t1); +INSERT INTO t2 VALUES + (1,'abc'), (2,'bcd'), (3,'cde'), (4,'def'), ( 5,'efg'), + (6,'fgh'), (7,'ghi'), (8,'hij'), (9,'ijk'), (10,'jkl'); +CREATE TABLE t3 (y int) INHERITS (t1); +INSERT INTO t3 VALUES (1,'xxx'),(2,'yyy'),(3,'zzz'),(4,'xyz'); + +ALTER TABLE t1 SET ROW LEVEL SECURITY (a % 4 = customer_age() % 4); +ALTER TABLE t2 SET ROW LEVEL SECURITY ((a+2) % 4 = customer_age() % 4); + +-- viewpoint from Alice +SET SESSION AUTHORIZATION rowlvsec_user1; +SELECT * FROM t1; +EXPLAIN (costs off) SELECT * FROM t1; + +BEGIN; +UPDATE t1 SET c = c || '_updt'; +EXPLAIN (costs off) UPDATE t1 SET c = c || '_updt'; +ABORT; +BEGIN; +DELETE FROM t1; +EXPLAIN (costs off) DELETE FROM t1; +ABORT; + +-- viewpoint from Carol +SET SESSION AUTHORIZATION rowlvsec_user3; +SELECT * FROM t1; +EXPLAIN (costs off) SELECT * FROM t1; + +BEGIN; +UPDATE t1 SET c = c || '_updt'; +ABORT; +BEGIN; +DELETE FROM t1; +ABORT; + +-- Clean up objects +RESET SESSION AUTHORIZATION; +DROP SCHEMA rowlvsec_schema CASCADE; +DROP USER rowlvsec_user3; +DROP USER rowlvsec_user2; +DROP USER rowlvsec_user1;