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;