[v9.4] row level security
The attached patch implements row-level security feature; that allows to
enforce a pre-configured security policy on reference of tables with the
row-level security policy.
It enables to isolate records to be visible from others according to access
control decision, usually done based on current user's credential.
It will make sense to ensure correctness of security for SaaS style
applications that typically share a common table for multiple users but
correctness of access control was ensured with correctness of application
itself.
Here is not functional update since the last commit fest for v9.3 except
for adjustment towards the latest master branch.
So, the explanation below might be bored for someone.
This feature enhances ALTER TABLE statement as follows:
ALTER TABLE <tablename> SET ROW SECURITY
FOR <command> TO (<expression>);
ALTER TABLE <tablename> RESET ROW SECURITY
FOR <command>;
<command> := { ALL | SELECT | INSERT | UPDATE | DELETE }
Right now, only "ALL" is supported command, even though syntax reserves
future enhancement allows to set individual security policy for each command.
The <expression> should be an expression that returns a bool value. It can
reference any column in the target table and contain sub-query that reference
another tables.
Then, the pre-configured expression shall be added when the table is referenced.
See below, it gives "(X % 2 = 1)" as security policy, user can see the record
that has odd-number at X. The EXPLAIN output below shows this expression
was automatically attached.
postgres=> ALTER TABLE tbl SET ROW SECURITY FOR ALL TO (x % 2 = 1);
ALTER TABLE
postgres=> EXPLAIN SELECT * FROM tbl WHERE y like '%abc%';
QUERY PLAN
-----------------------------------------------------------------
Subquery Scan on tbl (cost=0.00..28.52 rows=1 width=36)
Filter: (tbl.y ~~ '%abc%'::text)
-> Seq Scan on tbl tbl_1 (cost=0.00..28.45 rows=6 width=36)
Filter: ((x % 2) = 1)
(4 rows)
An important point is, reference to a particular relation is replaced
with a sub-
query that has security policy expression and security barrier attribute.
That prevent any (non leakproof) user given condition earlier than
security poliy
itself, thus, it ensures all records user can see is satisfies the
security policy.
On writer-queries, things to do are similar. It adds security policy expression
on the scan phase of UPDATE or DELETE statement. Thus, only visible records
are updatable or deletable.
postgres=> EXPLAIN UPDATE tbl SET y = y || '_update' WHERE y like '%xyz%';
QUERY PLAN
-----------------------------------------------------------------------
Update on tbl (cost=0.00..28.53 rows=1 width=42)
-> Subquery Scan on tbl_1 (cost=0.00..28.53 rows=1 width=42)
Filter: (tbl_1.y ~~ '%xyz%'::text)
-> Seq Scan on tbl tbl_2 (cost=0.00..28.45 rows=6 width=42)
Filter: ((x % 2) = 1)
(5 rows)
I had a relevant presentation at PGcon last month. I think its slides
are good summary
to know brief background of the long-standing problem.
http://www.pgcon.org/2013/schedule/attachments/273_PGcon2013-kaigai-row-level-security.pdf
Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
Attachments:
pgsql-v9.4-row-level-security.v1.patchapplication/octet-stream; name=pgsql-v9.4-row-level-security.v1.patchDownload
doc/src/sgml/catalogs.sgml | 65 ++
doc/src/sgml/ref/alter_table.sgml | 43 ++
doc/src/sgml/user-manag.sgml | 145 +++++
src/backend/catalog/Makefile | 4 +-
src/backend/catalog/dependency.c | 8 +
src/backend/catalog/heap.c | 1 +
src/backend/catalog/objectaddress.c | 50 ++
src/backend/catalog/pg_rowsecurity.c | 337 ++++++++++
src/backend/commands/copy.c | 90 ++-
src/backend/commands/event_trigger.c | 1 +
src/backend/commands/tablecmds.c | 27 +
src/backend/nodes/copyfuncs.c | 3 +
src/backend/nodes/equalfuncs.c | 3 +
src/backend/nodes/nodeFuncs.c | 12 +-
src/backend/nodes/outfuncs.c | 3 +
src/backend/nodes/readfuncs.c | 2 +
src/backend/optimizer/plan/planner.c | 23 +-
src/backend/optimizer/prep/preptlist.c | 61 +-
src/backend/optimizer/prep/prepunion.c | 95 ++-
src/backend/optimizer/util/Makefile | 2 +-
src/backend/optimizer/util/rowsecurity.c | 733 ++++++++++++++++++++++
src/backend/parser/gram.y | 25 +
src/backend/parser/parse_agg.c | 6 +
src/backend/parser/parse_expr.c | 3 +
src/backend/parser/parse_relation.c | 19 +
src/backend/rewrite/rewriteHandler.c | 16 +
src/backend/utils/adt/ri_triggers.c | 13 +-
src/backend/utils/cache/plancache.c | 32 +
src/backend/utils/cache/relcache.c | 17 +-
src/bin/pg_dump/common.c | 4 +
src/bin/pg_dump/pg_dump.c | 178 ++++++
src/bin/pg_dump/pg_dump.h | 13 +-
src/bin/pg_dump/pg_dump_sort.c | 5 +
src/bin/psql/describe.c | 7 +
src/include/catalog/dependency.h | 1 +
src/include/catalog/indexing.h | 5 +
src/include/catalog/pg_class.h | 24 +-
src/include/catalog/pg_rowsecurity.h | 76 +++
src/include/miscadmin.h | 1 +
src/include/nodes/execnodes.h | 4 +
src/include/nodes/nodeFuncs.h | 1 +
src/include/nodes/parsenodes.h | 14 +-
src/include/nodes/plannodes.h | 2 +
src/include/nodes/relation.h | 6 +
src/include/optimizer/rowsecurity.h | 27 +
src/include/parser/parse_node.h | 3 +-
src/include/parser/parsetree.h | 5 +-
src/include/rewrite/rewriteHandler.h | 1 +
src/include/utils/plancache.h | 2 +
src/include/utils/rel.h | 2 +
src/test/regress/expected/rowsecurity.out | 950 +++++++++++++++++++++++++++++
src/test/regress/expected/sanity_check.out | 3 +-
src/test/regress/parallel_schedule | 2 +-
src/test/regress/serial_schedule | 1 +
src/test/regress/sql/rowsecurity.sql | 298 +++++++++
55 files changed, 3432 insertions(+), 42 deletions(-)
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index e638a8f..85952ff 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -234,6 +234,11 @@
</row>
<row>
+ <entry><link linkend="catalog-pg-rowlevelsec"><structname>pg_rowlevelsec</structname></link></entry>
+ <entry>row-level security policy of relation</entry>
+ </row>
+
+ <row>
<entry><link linkend="catalog-pg-seclabel"><structname>pg_seclabel</structname></link></entry>
<entry>security labels on database objects</entry>
</row>
@@ -1857,6 +1862,16 @@
</row>
<row>
+ <entry><structfield>relhasrowsecurity</structfield></entry>
+ <entry><type>bool</type></entry>
+ <entry></entry>
+ <entry>
+ True if table has row-security policy; see
+ <link linkend="catalog-pg-rowsecurity"><structname>pg_rowsecurity</structname></link> catalog
+ </entry>
+ </row>
+
+ <row>
<entry><structfield>relhassubclass</structfield></entry>
<entry><type>bool</type></entry>
<entry></entry>
@@ -5094,6 +5109,56 @@
</sect1>
+ <sect1 id="catalog-pg-rowsecurity">
+ <title><structname>pg_rowlevelsec</structname></title>
+
+ <indexterm zone="catalog-pg-rowsecurity">
+ <primary>pg_rowsecurity</primary>
+ </indexterm>
+ <para>
+ The catalog <structname>pg_rowsecurity</structname> stores expression
+ tree of row-security policy to be performed on a particular relation.
+ </para>
+ <table>
+ <title><structname>pg_rowsecurity</structname> Columns</title>
+ <tgroup cols="4">
+ <thead>
+ <row>
+ <entry>Name</entry>
+ <entry>Type</entry>
+ <entry>References</entry>
+ <entry>Description</entry>
+ </row>
+ </thead>
+ <tbody>
+ <row>
+ <entry><structfield>rsecrelid</structfield></entry>
+ <entry><type>oid</type></entry>
+ <entry><literal><link linkend="catalog-pg-class"><structname>pg_class</structname></link>.oid</literal></entry>
+ <entry>The table this row-security is for</entry>
+ </row>
+ <row>
+ <entry><structfield>rseccmd</structfield></entry>
+ <entry><type>char</type></entry>
+ <entry></entry>
+ <entry>The command this row-security is for. 'a' meaning all is only possible value right now.</entry>
+ </row>
+ <row>
+ <entry><structfield>rsecqual</structfield></entry>
+ <entry><type>pg_node_tree</type></entry>
+ <entry></entry>
+ <entry>An expression tree to be performed as rowl-security policy</entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+ <note>
+ <para>
+ <literal>pg_class.relhasrowlevelsec</literal>
+ must be true if a table has row-level security policy in this catalog.
+ </para>
+ </note>
+ </sect1>
<sect1 id="catalog-pg-seclabel">
<title><structname>pg_seclabel</structname></title>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 5437626..41e5d1a 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -68,12 +68,16 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable>
NOT OF
OWNER TO <replaceable class="PARAMETER">new_owner</replaceable>
SET TABLESPACE <replaceable class="PARAMETER">new_tablespace</replaceable>
+ SET ROW SECURITY FOR <replaceable class="PARAMETER">rowsec_command</replaceable> TO (<replaceable class="PARAMETER">condition</replaceable>)
+ RESET ROW SECURITY FOR <replaceable class="PARAMETER">rowsec_command</replaceable>
<phrase>and <replaceable class="PARAMETER">table_constraint_using_index</replaceable> is:</phrase>
[ CONSTRAINT <replaceable class="PARAMETER">constraint_name</replaceable> ]
{ UNIQUE | PRIMARY KEY } USING INDEX <replaceable class="PARAMETER">index_name</replaceable>
[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+<phrase>and <replaceable class="PARAMETER">rowsec_command</replaceable> is:</phrase>
+ { ALL | SELECT | INSERT | UPDATE | DELETE }
</synopsis>
</refsynopsisdiv>
@@ -565,6 +569,31 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable>
</varlistentry>
<varlistentry>
+ <term><literal>SET ROW SECURITY FOR <replaceable class="PARAMETER">rowsec_command</replaceable> TO (<replaceable class="PARAMETER">condition</replaceable>)</literal></term>
+ <listitem>
+ <para>
+ This form set row-level security policy of the table.
+ Supplied <replaceable class="PARAMETER">condition</replaceable> performs
+ as if it is implicitly appended to the qualifiers of <literal>WHERE</literal>
+ clause, although mechanism guarantees to evaluate this condition earlier
+ than any other user given condition.
+ <literal>ALL</> is the only supported command type right now.
+ See also <xref linkend="ROW-SECURITY">.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>RESET ROW SECURITY FOR <replaceable class="PARAMETER">rowsec_command</replaceable></literal></term>
+ <listitem>
+ <para>
+ This form reset row-level security policy of the table, if exists.
+ <literal>ALL</> is the only supported command type right now.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
<term><literal>RENAME</literal></term>
<listitem>
<para>
@@ -806,6 +835,20 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable>
</listitem>
</varlistentry>
+ <varlistentry>
+ <term><replaceable class="PARAMETER">condition</replaceable></term>
+ <listitem>
+ <para>
+ An expression that returns a value of type boolean. Expect for a case
+ when queries are executed with superuser privilege, only rows for which
+ this expression returns true will be fetched, updated or deleted.
+ This expression can reference columns of the relation being configured.
+ Sub-queries can be contained within expression tree, unless referenced
+ relation recursively references the same relation.
+ </para>
+ </listitem>
+ </varlistentry>
+
</variablelist>
</refsect1>
diff --git a/doc/src/sgml/user-manag.sgml b/doc/src/sgml/user-manag.sgml
index 177ac7a..34a699e 100644
--- a/doc/src/sgml/user-manag.sgml
+++ b/doc/src/sgml/user-manag.sgml
@@ -439,4 +439,149 @@ DROP ROLE <replaceable>name</replaceable>;
</para>
</sect1>
+ <sect1 id="row-security">
+ <title>Row-security</title>
+ <para>
+ <productname>PostgreSQL</productname> v9.3 or later provides
+ row-security feature, like several commercial database
+ management system. It allows table owner to assign a particular
+ condition that performs as a security policy of the table; only
+ rows that satisfies the condition should be visible, except for
+ a case when superuser runs queries.
+ </para>
+ <para>
+ Row-security policy can be set using <literal>SET ROW SECURITY</literal>
+ command of <xref linkend="SQL-ALTERTABLE"> 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.
+ </para>
+ <para>
+ For example, the following <literal>customer</literal> table has
+ <literal>uname</literal> field to store user name, and it assume
+ we don't want to expose any properties of other customers.
+ The following command set <literal>current_user = uname</literal>
+ as row-security policy on the <literal>customer</literal> table.
+<screen>
+postgres=> ALTER TABLE customer SET ROW SECURITY
+ FOR ALL TO (current_user = uname);
+ALTER TABLE
+</screen>
+ <xref linkend="SQL-EXPLAIN"> command shows how row-security policy
+ works on the supplied query.
+<screen>
+postgres=> EXPLAIN(costs off) SELECT * FROM customer WHERE f_leak(upasswd);
+ QUERY PLAN
+--------------------------------------------
+ Subquery Scan on customer
+ Filter: f_leak(customer.upasswd)
+ -> Seq Scan on customer customer_1
+ Filter: ("current_user"() = uname)
+(4 rows)
+</screen>
+ This query execution plan means the preconfigured row-security policy is
+ implicitly added, and scan plan on the target relation being wrapped up
+ with a sub-query.
+ It ensures user given qualifiers, including functions with side effects,
+ are never executed earlier than the row-security policy regardless of
+ its cost, except for the cases when these were fully leakproof.
+ This design helps to tackle the scenario described in
+ <xref linkend="RULES-PRIVILEGES">; that introduces the order to evaluate
+ qualifiers is significant to keep confidentiality of invisible rows.
+ </para>
+
+ <para>
+ On the other hand, this design allows superusers to bypass checks with
+ row-security.
+ It ensures <application>pg_dump</application> can obtain a complete set
+ of database backup, and avoid to execute Trojan horse trap, being injected
+ as a row-security policy of user-defined table, with privileges of
+ superuser.
+ </para>
+
+ <para>
+ In case of queries on inherited tables, row-security policy of the parent
+ relation is not applied to child relations. Scope of the row-security
+ policy is limited to the relation on which it is set.
+<screen>
+postgres=> EXPLAIN(costs off) SELECT * FROM t1 WHERE f_leak(y);
+ QUERY PLAN
+-------------------------------------
+ Append
+ -> Subquery Scan on t1
+ Filter: f_leak(t1.y)
+ -> Seq Scan on t1 t1_1
+ Filter: ((x % 2) = 0)
+ -> Seq Scan on t2
+ Filter: f_leak(y)
+ -> Subquery Scan on t3
+ Filter: f_leak(t3.y)
+ -> Seq Scan on t3 t3_1
+ Filter: ((x % 2) = 1)
+(11 rows)
+</screen>
+ In the above example, <literal>t1</literal> has inherited
+ child table <literal>t2</literal> and <literal>t3</literal>,
+ and row-security policy is set on only <literal>t1</literal>,
+ and <literal>t3</literal>, not <literal>t2</literal>.
+
+ The row-security policy of <literal>t1</literal>, <literal>x</literal>
+ must be even-number, is appended only <literal>t1</literal>,
+ neither <literal>t2</literal> nor <literal>t3</literal>.
+ On the contrary, <literal>t3</literal> has different row-security policy;
+ <literal>x</literal> must be odd-number.
+ </para>
+
+ <para>
+ Row-security feature also works to queries for writer-operations; such as
+ <xref linkend="SQL-INSERT">, <xref linkend="SQL-UPDATE"> or
+ <xref linkend="SQL-DELETE"> commands.
+ It ensures all the modified rows satisfies configured row-security policy.
+ The below query tries to update e-mail address of the
+ <literal>customer</> table, and configured row-security makes sure all the
+ modified rows's <literal>uname</> field has to match with
+ <literal>current_user</>.
+ </para>
+<screen>
+postgres=> EXPLAIN (costs off) UPDATE customer SET email = 'alice@example.com';
+ QUERY PLAN
+--------------------------------------------------
+ Update on customer
+ -> Subquery Scan on customer
+ -> Seq Scan on customer customer_1
+ Filter: ("current_user"() = uname)
+(4 rows)
+</screen>
+ <para>
+ Modification of a certain table is consist of two different stuffs; one
+ is fetch rows to be modified from the result relation (except for
+ <literal>INSERT</> command), second is insertion of rows to the
+ result relation. Usually, before-row trigger or check constraints are
+ run in the second phase. In addition, row-security policy is also checked
+ on this stage, to prevent to insert or update rows with unprivileged
+ values.
+ </para>
+
+ <para>
+ Unlike other commercial database systems, we don't have any plan to allow
+ individual row-security policy for each command type. Even if we want to
+ perform with difference policy between <xref linkend="SQL-SELECT"> and
+ <xref linkend="SQL-DELETE">, <literal>RETURNING</> clause can leak the rows
+ to be invisible using <xref linkend="SQL-DELETE"> command.
+ </para>
+
+ <para>
+ Even though it is not a specific matter in row-security, please be careful
+ if you plan to use <literal>current_user</> in row-level security policy.
+ <xref linkend="SQL-DECLARE"> and <xref linkend="SQL-FETCH"> allows to
+ switch current user identifier during execution of the query.
+ Thus, it can fetch the first 100 rows with privilege of <literal>alice</>,
+ then remaining rows with privilege of <literal>bob</>. If and when query
+ execution plan contains some kind of materialization and row-security
+ policy contains <literal>current_user</>, the fetched tuples in
+ <literal>bob</>'s screen might be evaluated according to the privilege of
+ <literal>alice</>.
+ </para>
+ </sect1>
</chapter>
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index c4d3f3c..9b4e9f5 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -15,7 +15,7 @@ OBJS = catalog.o dependency.o heap.o index.o indexing.o namespace.o aclchk.o \
pg_constraint.o pg_conversion.o \
pg_depend.o pg_enum.o pg_inherits.o pg_largeobject.o pg_namespace.o \
pg_operator.o pg_proc.o pg_range.o pg_db_role_setting.o pg_shdepend.o \
- pg_type.o storage.o toasting.o
+ pg_rowsecurity.o pg_type.o storage.o toasting.o
BKIFILES = postgres.bki postgres.description postgres.shdescription
@@ -39,7 +39,7 @@ POSTGRES_BKI_SRCS = $(addprefix $(top_srcdir)/src/include/catalog/,\
pg_ts_config.h pg_ts_config_map.h pg_ts_dict.h \
pg_ts_parser.h pg_ts_template.h pg_extension.h \
pg_foreign_data_wrapper.h pg_foreign_server.h pg_user_mapping.h \
- pg_foreign_table.h \
+ pg_foreign_table.h pg_rowsecurity.h \
pg_default_acl.h pg_seclabel.h pg_shseclabel.h pg_collation.h pg_range.h \
toasting.h indexing.h \
)
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index 69171f8..2b2178a 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_rowsecurity.h"
#include "catalog/pg_tablespace.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_ts_config.h"
@@ -1249,6 +1250,10 @@ doDeletion(const ObjectAddress *object, int flags)
RemoveEventTriggerById(object->objectId);
break;
+ case OCLASS_ROWSECURITY:
+ RemoveRowSecurityById(object->objectId);
+ break;
+
default:
elog(ERROR, "unrecognized object class: %u",
object->classId);
@@ -2308,6 +2313,9 @@ getObjectClass(const ObjectAddress *object)
case EventTriggerRelationId:
return OCLASS_EVENT_TRIGGER;
+
+ case RowSecurityRelationId:
+ return OCLASS_ROWSECURITY;
}
/* shouldn't get here */
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 45a84e4..3580368 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -792,6 +792,7 @@ InsertPgClassTuple(Relation pg_class_desc,
values[Anum_pg_class_relhaspkey - 1] = BoolGetDatum(rd_rel->relhaspkey);
values[Anum_pg_class_relhasrules - 1] = BoolGetDatum(rd_rel->relhasrules);
values[Anum_pg_class_relhastriggers - 1] = BoolGetDatum(rd_rel->relhastriggers);
+ values[Anum_pg_class_relhasrowsecurity - 1] = BoolGetDatum(rd_rel->relhasrowsecurity);
values[Anum_pg_class_relhassubclass - 1] = BoolGetDatum(rd_rel->relhassubclass);
values[Anum_pg_class_relispopulated - 1] = BoolGetDatum(rd_rel->relispopulated);
values[Anum_pg_class_relfrozenxid - 1] = TransactionIdGetDatum(rd_rel->relfrozenxid);
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 215eaf5..baf6a24 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -2143,6 +2143,56 @@ getObjectDescription(const ObjectAddress *object)
break;
}
+ case OCLASS_ROWSECURITY:
+ {
+ Relation rsec_rel;
+ ScanKeyData skey;
+ SysScanDesc sscan;
+ HeapTuple tuple;
+ Form_pg_rowsecurity form_rsec;
+
+ rsec_rel = heap_open(RowSecurityRelationId, AccessShareLock);
+
+ ScanKeyInit(&skey,
+ ObjectIdAttributeNumber,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(object->objectId));
+ sscan = systable_beginscan(rsec_rel, RowSecurityOidIndexId,
+ true, SnapshotNow, 1, &skey);
+ tuple = systable_getnext(sscan);
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "could not find tuple for row-security %u",
+ object->objectId);
+ form_rsec = (Form_pg_rowsecurity) GETSTRUCT(tuple);
+
+ appendStringInfo(&buffer, _("row-security of "));
+ getRelationDescription(&buffer, form_rsec->rsecrelid);
+ switch (form_rsec->rseccmd)
+ {
+ case ROWSECURITY_CMD_ALL:
+ appendStringInfo(&buffer, _(" FOR ALL"));
+ break;
+ case ROWSECURITY_CMD_SELECT:
+ appendStringInfo(&buffer, _(" FOR SELECT"));
+ break;
+ case ROWSECURITY_CMD_INSERT:
+ appendStringInfo(&buffer, _(" FOR INSERT"));
+ break;
+ case ROWSECURITY_CMD_UPDATE:
+ appendStringInfo(&buffer, _(" FOR UPDATE"));
+ break;
+ case ROWSECURITY_CMD_DELETE:
+ appendStringInfo(&buffer, _(" FOR DELETE"));
+ break;
+ default:
+ elog(ERROR, "unexpected row-security command type: %c",
+ form_rsec->rseccmd);
+ }
+ systable_endscan(sscan);
+ heap_close(rsec_rel, AccessShareLock);
+ break;
+ }
+
default:
appendStringInfo(&buffer, "unrecognized object %u %u %d",
object->classId,
diff --git a/src/backend/catalog/pg_rowsecurity.c b/src/backend/catalog/pg_rowsecurity.c
new file mode 100644
index 0000000..25739ff
--- /dev/null
+++ b/src/backend/catalog/pg_rowsecurity.c
@@ -0,0 +1,337 @@
+/* -------------------------------------------------------------------------
+ *
+ * pg_rowsecurity.c
+ * routines to support manipulation of the pg_rowsecurity catalog
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * -------------------------------------------------------------------------
+ */
+#include "postgres.h"
+#include "access/genam.h"
+#include "access/heapam.h"
+#include "access/htup_details.h"
+#include "access/sysattr.h"
+#include "catalog/dependency.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_rowsecurity.h"
+#include "catalog/pg_type.h"
+#include "nodes/nodeFuncs.h"
+#include "optimizer/clauses.h"
+#include "parser/parse_clause.h"
+#include "parser/parse_node.h"
+#include "parser/parse_relation.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/inval.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/tqual.h"
+
+/*
+ * Load row-security policy from the catalog, and keep it on
+ * the relation cache.
+ */
+void
+RelationBuildRowSecurity(Relation relation)
+{
+ Relation catalog;
+ ScanKeyData skey;
+ SysScanDesc sscan;
+ HeapTuple tuple;
+ MemoryContext oldcxt;
+ MemoryContext rscxt = NULL;
+ RowSecurityDesc *rsdesc = NULL;
+
+ catalog = heap_open(RowSecurityRelationId, AccessShareLock);
+
+ ScanKeyInit(&skey,
+ Anum_pg_rowsecurity_rsecrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(RelationGetRelid(relation)));
+ sscan = systable_beginscan(catalog, RowSecurityRelidIndexId, true,
+ SnapshotNow, 1, &skey);
+ PG_TRY();
+ {
+ while (HeapTupleIsValid(tuple = systable_getnext(sscan)))
+ {
+ Datum value;
+ bool isnull;
+ char *temp;
+
+ if (!rsdesc)
+ {
+ rscxt = AllocSetContextCreate(CacheMemoryContext,
+ "Row-security descriptor",
+ ALLOCSET_SMALL_MINSIZE,
+ ALLOCSET_SMALL_INITSIZE,
+ ALLOCSET_SMALL_MAXSIZE);
+ oldcxt = MemoryContextSwitchTo(rscxt);
+ rsdesc = palloc0(sizeof(RowSecurityDesc));
+ rsdesc->rscxt = rscxt;
+ MemoryContextSwitchTo(oldcxt);
+ }
+ value = heap_getattr(tuple, Anum_pg_rowsecurity_rseccmd,
+ RelationGetDescr(catalog), &isnull);
+ Assert(!isnull);
+
+ if (DatumGetChar(value) != ROWSECURITY_CMD_ALL)
+ {
+ ereport(WARNING,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("Per-command row-security not implemented")));
+ continue;
+ }
+
+ value = heap_getattr(tuple, Anum_pg_rowsecurity_rsecqual,
+ RelationGetDescr(catalog), &isnull);
+ Assert(!isnull);
+ temp = TextDatumGetCString(value);
+
+ oldcxt = MemoryContextSwitchTo(rscxt);
+ rsdesc->rsall.rsecid = HeapTupleGetOid(tuple);
+ rsdesc->rsall.qual = (Expr *) stringToNode(temp);
+ Assert(exprType((Node *)rsdesc->rsall.qual) == BOOLOID);
+ rsdesc->rsall.hassublinks
+ = contain_subplans((Node *)rsdesc->rsall.qual);
+ MemoryContextSwitchTo(oldcxt);
+
+ pfree(temp);
+ }
+ }
+ PG_CATCH();
+ {
+ if (rscxt != NULL)
+ MemoryContextDelete(rscxt);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ systable_endscan(sscan);
+ heap_close(catalog, AccessShareLock);
+
+ relation->rsdesc = rsdesc;
+}
+
+/*
+ * Parse the supplied row-security policy, and insert/update a row
+ * of pg_rowsecurity catalog.
+ */
+static void
+InsertOrUpdatePolicyRow(Relation relation, char rseccmd, Node *clause)
+{
+ Oid relationId = RelationGetRelid(relation);
+ Oid rowsecId;
+ ParseState *pstate;
+ RangeTblEntry *rte;
+ Node *qual;
+ Relation catalog;
+ ScanKeyData skeys[2];
+ SysScanDesc sscan;
+ HeapTuple oldtup;
+ HeapTuple newtup;
+ Datum values[Natts_pg_rowsecurity];
+ bool isnull[Natts_pg_rowsecurity];
+ bool replaces[Natts_pg_rowsecurity];
+ ObjectAddress target;
+ ObjectAddress myself;
+
+ /* Parse the supplied clause */
+ pstate = make_parsestate(NULL);
+
+ rte = addRangeTableEntryForRelation(pstate, relation,
+ NULL, false, false);
+ addRTEtoQuery(pstate, rte, false, true, true);
+
+ qual = transformWhereClause(pstate, copyObject(clause),
+ EXPR_KIND_ROW_SECURITY,
+ "ROW SECURITY");
+ /* zero-clear */
+ memset(values, 0, sizeof(values));
+ memset(replaces, 0, sizeof(replaces));
+ memset(isnull, 0, sizeof(isnull));
+
+ /* Update or Insert an entry to pg_rowsecurity catalog */
+ catalog = heap_open(RowSecurityRelationId, RowExclusiveLock);
+
+ ScanKeyInit(&skeys[0],
+ Anum_pg_rowsecurity_rsecrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(RelationGetRelid(relation)));
+ ScanKeyInit(&skeys[1],
+ Anum_pg_rowsecurity_rseccmd,
+ BTEqualStrategyNumber, F_CHAREQ,
+ CharGetDatum(rseccmd));
+ sscan = systable_beginscan(catalog, RowSecurityRelidIndexId, true,
+ SnapshotNow, 2, skeys);
+ oldtup = systable_getnext(sscan);
+ if (HeapTupleIsValid(oldtup))
+ {
+ rowsecId = HeapTupleGetOid(oldtup);
+
+ replaces[Anum_pg_rowsecurity_rsecqual - 1] = true;
+ values[Anum_pg_rowsecurity_rsecqual - 1]
+ = CStringGetTextDatum(nodeToString(qual));
+
+ newtup = heap_modify_tuple(oldtup,
+ RelationGetDescr(catalog),
+ values, isnull, replaces);
+ simple_heap_update(catalog, &newtup->t_self, newtup);
+
+ deleteDependencyRecordsFor(RowSecurityRelationId, rowsecId, false);
+ }
+ else
+ {
+ values[Anum_pg_rowsecurity_rsecrelid - 1]
+ = ObjectIdGetDatum(relationId);
+ values[Anum_pg_rowsecurity_rseccmd - 1]
+ = CharGetDatum(rseccmd);
+ values[Anum_pg_rowsecurity_rsecqual - 1]
+ = CStringGetTextDatum(nodeToString(qual));
+ newtup = heap_form_tuple(RelationGetDescr(catalog),
+ values, isnull);
+ rowsecId = simple_heap_insert(catalog, newtup);
+ }
+ CatalogUpdateIndexes(catalog, newtup);
+
+ heap_freetuple(newtup);
+
+ /* records dependencies of row-security policy and relation/columns */
+ target.classId = RelationRelationId;
+ target.objectId = relationId;
+ target.objectSubId = 0;
+
+ myself.classId = RowSecurityRelationId;
+ myself.objectId = rowsecId;
+ myself.objectSubId = 0;
+
+ recordDependencyOn(&myself, &target, DEPENDENCY_AUTO);
+
+ recordDependencyOnExpr(&myself, qual, pstate->p_rtable,
+ DEPENDENCY_NORMAL);
+ free_parsestate(pstate);
+
+ systable_endscan(sscan);
+ heap_close(catalog, RowExclusiveLock);
+}
+
+/*
+ * Remove row-security policy row of pg_rowsecurity
+ */
+static void
+DeletePolicyRow(Relation relation, char rseccmd)
+{
+ Assert(rseccmd == ROWSECURITY_CMD_ALL);
+
+ if (relation->rsdesc)
+ {
+ ObjectAddress address;
+
+ address.classId = RowSecurityRelationId;
+ address.objectId = relation->rsdesc->rsall.rsecid;
+ address.objectSubId = 0;
+
+ performDeletion(&address, DROP_RESTRICT, 0);
+ }
+ else
+ {
+ /* Nothing to do here */
+ elog(INFO, "relation %s has no row-security policy, skipped",
+ RelationGetRelationName(relation));
+ }
+}
+
+/*
+ * Guts of row-security policy deletion.
+ */
+void
+RemoveRowSecurityById(Oid rowsecId)
+{
+ Relation catalog;
+ ScanKeyData skey;
+ SysScanDesc sscan;
+ HeapTuple tuple;
+ Relation rel;
+ Oid relid;
+
+ catalog = heap_open(RowSecurityRelationId, RowExclusiveLock);
+
+ ScanKeyInit(&skey,
+ ObjectIdAttributeNumber,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(rowsecId));
+ sscan = systable_beginscan(catalog, RowSecurityOidIndexId, true,
+ SnapshotNow, 1, &skey);
+ tuple = systable_getnext(sscan);
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "could not find tuple for row-security %u", rowsecId);
+
+ /*
+ * Open and exclusive-lock the relation the row-security belongs to.
+ */
+ relid = ((Form_pg_rowsecurity) GETSTRUCT(tuple))->rsecrelid;
+
+ rel = heap_open(relid, AccessExclusiveLock);
+
+ simple_heap_delete(catalog, &tuple->t_self);
+
+ /* Ensure relcache entries of other session being rebuilt */
+ CacheInvalidateRelcache(rel);
+
+ heap_close(rel, NoLock);
+
+ systable_endscan(sscan);
+ heap_close(catalog, RowExclusiveLock);
+}
+
+/*
+ * ALTER TABLE <name> SET ROW SECURITY (...) OR
+ * RESET ROW SECURITY
+ */
+void
+ATExecSetRowSecurity(Relation relation, const char *cmdname, Node *clause)
+{
+ Oid relid = RelationGetRelid(relation);
+ char rseccmd;
+
+ if (strcmp(cmdname, "all") == 0)
+ rseccmd = ROWSECURITY_CMD_ALL;
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("Row-security for \"%s\" is not implemented yet",
+ cmdname)));
+
+ if (clause != NULL)
+ {
+ InsertOrUpdatePolicyRow(relation, rseccmd, clause);
+
+ /*
+ * Also, turn on relhasrowsecurity, if not.
+ */
+ if (!RelationGetForm(relation)->relhasrowsecurity)
+ {
+ Relation class_rel = heap_open(RelationRelationId,
+ RowExclusiveLock);
+ HeapTuple tuple;
+
+ tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for relation %u", relid);
+
+ ((Form_pg_class) GETSTRUCT(tuple))->relhasrowsecurity = true;
+
+ simple_heap_update(class_rel, &tuple->t_self, tuple);
+ CatalogUpdateIndexes(class_rel, tuple);
+
+ heap_freetuple(tuple);
+ heap_close(class_rel, RowExclusiveLock);
+ }
+ }
+ else
+ DeletePolicyRow(relation, rseccmd);
+
+ CacheInvalidateRelcache(relation);
+}
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 31819cc..9cb8f1b 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -24,6 +24,7 @@
#include "access/htup_details.h"
#include "access/sysattr.h"
#include "access/xact.h"
+#include "catalog/heap.h"
#include "catalog/namespace.h"
#include "catalog/pg_type.h"
#include "commands/copy.h"
@@ -34,15 +35,19 @@
#include "libpq/pqformat.h"
#include "mb/pg_wchar.h"
#include "miscadmin.h"
+#include "nodes/makefuncs.h"
#include "optimizer/clauses.h"
#include "optimizer/planner.h"
+#include "optimizer/rowsecurity.h"
#include "parser/parse_relation.h"
+#include "parser/parsetree.h"
#include "rewrite/rewriteHandler.h"
#include "storage/fd.h"
#include "tcop/tcopprot.h"
#include "utils/acl.h"
#include "utils/builtins.h"
#include "utils/lsyscache.h"
+#include "utils/syscache.h"
#include "utils/memutils.h"
#include "utils/portal.h"
#include "utils/rel.h"
@@ -814,6 +819,21 @@ DoCopy(const CopyStmt *stmt, const char *queryString, uint64 *processed)
rel = heap_openrv(stmt->relation,
(is_from ? RowExclusiveLock : AccessShareLock));
+ tupDesc = RelationGetDescr(rel);
+ attnums = CopyGetAttnums(tupDesc, rel, stmt->attlist);
+
+ /*
+ * We have to run regular query, if the target relation has
+ * row-level security policy
+ */
+ if (copy_row_security_policy((CopyStmt *)stmt, rel, attnums))
+ {
+ heap_close(rel, NoLock); /* close with keeping lock */
+ relid = InvalidOid;
+ rel = NULL;
+ }
+ else
+ {
relid = RelationGetRelid(rel);
rte = makeNode(RangeTblEntry);
@@ -822,8 +842,6 @@ DoCopy(const CopyStmt *stmt, const char *queryString, uint64 *processed)
rte->relkind = rel->rd_rel->relkind;
rte->requiredPerms = required_access;
- tupDesc = RelationGetDescr(rel);
- attnums = CopyGetAttnums(tupDesc, rel, stmt->attlist);
foreach(cur, attnums)
{
int attno = lfirst_int(cur) -
@@ -835,6 +853,7 @@ DoCopy(const CopyStmt *stmt, const char *queryString, uint64 *processed)
rte->selectedCols = bms_add_member(rte->selectedCols, attno);
}
ExecCheckRTPerms(list_make1(rte), true);
+ }
}
else
{
@@ -1193,6 +1212,53 @@ ProcessCopyOptions(CopyState cstate,
}
/*
+ * Adjust Query tree constructed with row-level security feature.
+ * If WITH OIDS option was supplied, it adds Var node to reference
+ * object-id system column.
+ */
+static void
+fixup_oid_of_rls_query(Query *query)
+{
+ RangeTblEntry *subrte;
+ TargetEntry *subtle;
+ Var *subvar;
+ ListCell *cell;
+ Form_pg_attribute attform
+ = SystemAttributeDefinition(ObjectIdAttributeNumber, true);
+
+ subrte = rt_fetch((Index) 1, query->rtable);
+ Assert(subrte->rtekind == RTE_RELATION);
+
+ if (!SearchSysCacheExists2(ATTNUM,
+ ObjectIdGetDatum(subrte->relid),
+ Int16GetDatum(attform->attnum)))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_COLUMN),
+ errmsg("table \"%s\" does not have OIDs",
+ get_rel_name(subrte->relid))));
+
+ subvar = makeVar((Index) 1,
+ attform->attnum,
+ attform->atttypid,
+ attform->atttypmod,
+ attform->attcollation,
+ 0);
+ subtle = makeTargetEntry((Expr *) subvar,
+ 0,
+ pstrdup(NameStr(attform->attname)),
+ false);
+
+ query->targetList = list_concat(list_make1(subtle),
+ query->targetList);
+ /* adjust resno of TargetEntry */
+ foreach (cell, query->targetList)
+ {
+ subtle = lfirst(cell);
+ subtle->resno++;
+ }
+}
+
+/*
* Common setup routines used by BeginCopyFrom and BeginCopyTo.
*
* Iff <binary>, unload or reload in the binary format, as opposed to the
@@ -1264,6 +1330,25 @@ BeginCopy(bool is_from,
Assert(!is_from);
cstate->rel = NULL;
+ /*
+ * In case when regular COPY TO was replaced because of row-level
+ * security, "raw_query" node have already analyzed / rewritten
+ * query tree.
+ */
+ if (IsA(raw_query, Query))
+ {
+ query = (Query *) raw_query;
+
+ Assert(query->querySource == QSRC_ROW_SECURITY);
+ if (cstate->oids)
+ {
+ fixup_oid_of_rls_query(query);
+ cstate->oids = false;
+ }
+ attnamelist = NIL;
+ }
+ else
+ {
/* Don't allow COPY w/ OIDs from a select */
if (cstate->oids)
ereport(ERROR,
@@ -1288,6 +1373,7 @@ BeginCopy(bool is_from,
elog(ERROR, "unexpected rewrite result");
query = (Query *) linitial(rewritten);
+ }
/* The grammar allows SELECT INTO, but we don't support that */
if (query->utilityStmt != NULL &&
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index a0f97e4..9289a64 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -988,6 +988,7 @@ EventTriggerSupportsObjectClass(ObjectClass objclass)
case OCLASS_USER_MAPPING:
case OCLASS_DEFACL:
case OCLASS_EXTENSION:
+ case OCLASS_ROWSECURITY:
return true;
case MAX_OCLASS:
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 8294b29..62766d6 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -37,6 +37,7 @@
#include "catalog/pg_inherits_fn.h"
#include "catalog/pg_namespace.h"
#include "catalog/pg_opclass.h"
+#include "catalog/pg_rowsecurity.h"
#include "catalog/pg_tablespace.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
@@ -2784,6 +2785,8 @@ AlterTableGetLockLevel(List *cmds)
case AT_SetTableSpace: /* must rewrite heap */
case AT_DropNotNull: /* may change some SQL plans */
case AT_SetNotNull:
+ case AT_SetRowSecurity:
+ case AT_ResetRowSecurity:
case AT_GenericOptions:
case AT_AlterColumnGenericOptions:
cmd_lockmode = AccessExclusiveLock;
@@ -3147,6 +3150,8 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
case AT_DropInherit: /* NO INHERIT */
case AT_AddOf: /* OF */
case AT_DropOf: /* NOT OF */
+ case AT_SetRowSecurity:
+ case AT_ResetRowSecurity:
ATSimplePermissions(rel, ATT_TABLE);
/* These commands never recurse */
/* No command-specific prep needed */
@@ -3428,6 +3433,12 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab, Relation rel,
case AT_DropOf:
ATExecDropOf(rel, lockmode);
break;
+ case AT_SetRowSecurity:
+ ATExecSetRowSecurity(rel, cmd->name, (Node *) cmd->def);
+ break;
+ case AT_ResetRowSecurity:
+ ATExecSetRowSecurity(rel, cmd->name, NULL);
+ break;
case AT_GenericOptions:
ATExecGenericOptions(rel, (List *) cmd->def);
break;
@@ -7638,6 +7649,22 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
Assert(defaultexpr);
break;
+ case OCLASS_ROWSECURITY:
+ /*
+ * A row-level security policy can depend on a column in case
+ * when the policy clause references a particular column.
+ * Due to same reason why TRIGGER ... WHEN does not support
+ * to change column's type being referenced in clause, row-
+ * level security policy also does not support it.
+ */
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot alter type of a column used in a row-level security policy"),
+ errdetail("%s depends on column \"%s\"",
+ getObjectDescription(&foundObject),
+ colName)));
+ break;
+
case OCLASS_PROC:
case OCLASS_TYPE:
case OCLASS_CAST:
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index b5b8d63..0835bda 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -1933,6 +1933,7 @@ _copyAppendRelInfo(const AppendRelInfo *from)
COPY_SCALAR_FIELD(parent_relid);
COPY_SCALAR_FIELD(child_relid);
+ COPY_SCALAR_FIELD(child_result);
COPY_SCALAR_FIELD(parent_reltype);
COPY_SCALAR_FIELD(child_reltype);
COPY_NODE_FIELD(translated_vars);
@@ -1974,6 +1975,7 @@ _copyRangeTblEntry(const RangeTblEntry *from)
COPY_SCALAR_FIELD(relkind);
COPY_NODE_FIELD(subquery);
COPY_SCALAR_FIELD(security_barrier);
+ COPY_SCALAR_FIELD(rowsec_relid);
COPY_SCALAR_FIELD(jointype);
COPY_NODE_FIELD(joinaliasvars);
COPY_NODE_FIELD(funcexpr);
@@ -2432,6 +2434,7 @@ _copyQuery(const Query *from)
COPY_SCALAR_FIELD(canSetTag);
COPY_NODE_FIELD(utilityStmt);
COPY_SCALAR_FIELD(resultRelation);
+ COPY_SCALAR_FIELD(sourceRelation);
COPY_SCALAR_FIELD(hasAggs);
COPY_SCALAR_FIELD(hasWindowFuncs);
COPY_SCALAR_FIELD(hasSubLinks);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 3f96595..f688275 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -805,6 +805,7 @@ _equalAppendRelInfo(const AppendRelInfo *a, const AppendRelInfo *b)
{
COMPARE_SCALAR_FIELD(parent_relid);
COMPARE_SCALAR_FIELD(child_relid);
+ COMPARE_SCALAR_FIELD(child_result);
COMPARE_SCALAR_FIELD(parent_reltype);
COMPARE_SCALAR_FIELD(child_reltype);
COMPARE_NODE_FIELD(translated_vars);
@@ -840,6 +841,7 @@ _equalQuery(const Query *a, const Query *b)
COMPARE_SCALAR_FIELD(canSetTag);
COMPARE_NODE_FIELD(utilityStmt);
COMPARE_SCALAR_FIELD(resultRelation);
+ COMPARE_SCALAR_FIELD(sourceRelation);
COMPARE_SCALAR_FIELD(hasAggs);
COMPARE_SCALAR_FIELD(hasWindowFuncs);
COMPARE_SCALAR_FIELD(hasSubLinks);
@@ -2222,6 +2224,7 @@ _equalRangeTblEntry(const RangeTblEntry *a, const RangeTblEntry *b)
COMPARE_SCALAR_FIELD(relkind);
COMPARE_NODE_FIELD(subquery);
COMPARE_SCALAR_FIELD(security_barrier);
+ COMPARE_SCALAR_FIELD(rowsec_relid);
COMPARE_SCALAR_FIELD(jointype);
COMPARE_NODE_FIELD(joinaliasvars);
COMPARE_NODE_FIELD(funcexpr);
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 42d6621..2b265dd 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -1869,8 +1869,11 @@ query_tree_walker(Query *query,
if (walker((Node *) query->targetList, context))
return true;
- if (walker((Node *) query->returningList, context))
- return true;
+ if (!(flags & QTW_IGNORE_RETURNING))
+ {
+ if (walker((Node *) query->returningList, context))
+ return true;
+ }
if (walker((Node *) query->jointree, context))
return true;
if (walker(query->setOperations, context))
@@ -2583,7 +2586,10 @@ query_tree_mutator(Query *query,
}
MUTATE(query->targetList, query->targetList, List *);
- MUTATE(query->returningList, query->returningList, List *);
+ if (!(flags & QTW_IGNORE_RETURNING))
+ MUTATE(query->returningList, query->returningList, List *);
+ else
+ query->returningList = copyObject(query->returningList);
MUTATE(query->jointree, query->jointree, FromExpr *);
MUTATE(query->setOperations, query->setOperations, Node *);
MUTATE(query->havingQual, query->havingQual, Node *);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index b2183f4..81d7774 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -1920,6 +1920,7 @@ _outAppendRelInfo(StringInfo str, const AppendRelInfo *node)
WRITE_UINT_FIELD(parent_relid);
WRITE_UINT_FIELD(child_relid);
+ WRITE_UINT_FIELD(child_result);
WRITE_OID_FIELD(parent_reltype);
WRITE_OID_FIELD(child_reltype);
WRITE_NODE_FIELD(translated_vars);
@@ -2233,6 +2234,7 @@ _outQuery(StringInfo str, const Query *node)
appendStringInfo(str, " :utilityStmt <>");
WRITE_INT_FIELD(resultRelation);
+ WRITE_INT_FIELD(sourceRelation);
WRITE_BOOL_FIELD(hasAggs);
WRITE_BOOL_FIELD(hasWindowFuncs);
WRITE_BOOL_FIELD(hasSubLinks);
@@ -2357,6 +2359,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node)
case RTE_SUBQUERY:
WRITE_NODE_FIELD(subquery);
WRITE_BOOL_FIELD(security_barrier);
+ WRITE_OID_FIELD(rowsec_relid);
break;
case RTE_JOIN:
WRITE_ENUM_FIELD(jointype, JoinType);
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 3a16e9d..1a3da23 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -199,6 +199,7 @@ _readQuery(void)
READ_BOOL_FIELD(canSetTag);
READ_NODE_FIELD(utilityStmt);
READ_INT_FIELD(resultRelation);
+ READ_INT_FIELD(sourceRelation);
READ_BOOL_FIELD(hasAggs);
READ_BOOL_FIELD(hasWindowFuncs);
READ_BOOL_FIELD(hasSubLinks);
@@ -1195,6 +1196,7 @@ _readRangeTblEntry(void)
case RTE_SUBQUERY:
READ_NODE_FIELD(subquery);
READ_BOOL_FIELD(security_barrier);
+ READ_OID_FIELD(rowsec_relid);
break;
case RTE_JOIN:
READ_ENUM_FIELD(jointype, JoinType);
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index d80c264..976b9c0 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -33,6 +33,7 @@
#include "optimizer/planmain.h"
#include "optimizer/planner.h"
#include "optimizer/prep.h"
+#include "optimizer/rowsecurity.h"
#include "optimizer/subselect.h"
#include "optimizer/tlist.h"
#include "parser/analyze.h"
@@ -176,6 +177,7 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams)
glob->lastPHId = 0;
glob->lastRowMarkId = 0;
glob->transientPlan = false;
+ glob->planUserId = InvalidOid;
/* Determine what fraction of the plan is likely to be scanned */
if (cursorOptions & CURSOR_OPT_FAST_PLAN)
@@ -253,6 +255,7 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams)
result->relationOids = glob->relationOids;
result->invalItems = glob->invalItems;
result->nParamExec = glob->nParamExec;
+ result->planUserId = glob->planUserId;
return result;
}
@@ -402,6 +405,19 @@ subquery_planner(PlannerGlobal *glob, Query *parse,
expand_inherited_tables(root);
/*
+ * Apply row-security policy of the relation being referenced,
+ * if configured with either of built-in or extension's features.
+ * RangeTblEntry of the relation with row-security policy shall
+ * be replaced with a row-security subquery that has simple scan
+ * on the target relation with row-security policy qualifiers.
+ *
+ * This routine assumes PlannerInfo is already handled with
+ * expand_inherited_tables, thus, AppendRelInfo or PlanRowMark
+ * have valid information.
+ */
+ apply_row_security_policy(root);
+
+ /*
* Set hasHavingQual to remember if HAVING clause is present. Needed
* because preprocess_expression will reduce a constant-true condition to
* an empty qual list ... but "HAVING TRUE" is not a semantic no-op.
@@ -865,6 +881,8 @@ inheritance_planner(PlannerInfo *root)
newrti = list_length(subroot.parse->rtable) + 1;
ChangeVarNodes((Node *) subroot.parse, rti, newrti, 0);
ChangeVarNodes((Node *) subroot.rowMarks, rti, newrti, 0);
+ if (subroot.parse->sourceRelation == rti)
+ subroot.parse->sourceRelation = newrti;
rte = copyObject(rte);
subroot.parse->rtable = lappend(subroot.parse->rtable,
rte);
@@ -928,7 +946,10 @@ inheritance_planner(PlannerInfo *root)
root->init_plans = subroot.init_plans;
/* Build list of target-relation RT indexes */
- resultRelations = lappend_int(resultRelations, appinfo->child_relid);
+ resultRelations = lappend_int(resultRelations,
+ (appinfo->child_result > 0 ?
+ appinfo->child_result :
+ appinfo->child_relid));
/* Build list of per-relation RETURNING targetlists */
if (parse->returningList)
diff --git a/src/backend/optimizer/prep/preptlist.c b/src/backend/optimizer/prep/preptlist.c
index fb67f9e..4abf434 100644
--- a/src/backend/optimizer/prep/preptlist.c
+++ b/src/backend/optimizer/prep/preptlist.c
@@ -37,8 +37,48 @@
static List *expand_targetlist(List *tlist, int command_type,
- Index result_relation, List *range_table);
+ 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 Var that references sub-queries being originated from regular
+ * relations with row-level security policy due to nature of sub-query
+ * that has no system-column.
+ */
+static AttrNumber
+lookup_varattno(AttrNumber attno, Index rt_index, List *rtables)
+{
+ RangeTblEntry *rte = rt_fetch(rt_index, rtables);
+
+ if (rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_SECURITY)
+ {
+ ListCell *cell;
+
+ foreach (cell, rte->subquery->targetList)
+ {
+ TargetEntry *tle = lfirst(cell);
+ Var *var;
+
+ if (IsA(tle->expr, Const))
+ continue;
+
+ var = (Var *) tle->expr;
+ Assert(IsA(var, Var));
+
+ if (var->varattno == attno)
+ return tle->resno;
+ }
+ elog(ERROR, "invalid attno %d on row-security subquery target-list",
+ attno);
+ }
+ return attno;
+}
/*
* preprocess_targetlist
@@ -73,8 +113,13 @@ preprocess_targetlist(PlannerInfo *root, List *tlist)
* 10/94
*/
if (command_type == CMD_INSERT || command_type == CMD_UPDATE)
+ {
+ Index source_relation = (parse->sourceRelation > 0 ?
+ parse->sourceRelation :
+ result_relation);
tlist = expand_targetlist(tlist, command_type,
- result_relation, range_table);
+ source_relation, range_table);
+ }
/*
* Add necessary junk columns for rowmarked rels. These values are needed
@@ -96,7 +141,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,
@@ -112,7 +158,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,
@@ -130,7 +177,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,
@@ -299,7 +346,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 e249628..0cbb035 100644
--- a/src/backend/optimizer/prep/prepunion.c
+++ b/src/backend/optimizer/prep/prepunion.c
@@ -55,6 +55,7 @@ typedef struct
{
PlannerInfo *root;
AppendRelInfo *appinfo;
+ bool in_returning;
} adjust_appendrel_attrs_context;
static Plan *recurse_set_operations(Node *setOp, PlannerInfo *root,
@@ -1594,6 +1595,7 @@ adjust_appendrel_attrs(PlannerInfo *root, Node *node, AppendRelInfo *appinfo)
context.root = root;
context.appinfo = appinfo;
+ context.in_returning = false;
/*
* Must be prepared to start with a Query or a bare expression tree.
@@ -1605,10 +1607,29 @@ adjust_appendrel_attrs(PlannerInfo *root, Node *node, AppendRelInfo *appinfo)
newnode = query_tree_mutator((Query *) node,
adjust_appendrel_attrs_mutator,
(void *) &context,
- QTW_IGNORE_RC_SUBQUERIES);
+ QTW_IGNORE_RC_SUBQUERIES |
+ QTW_IGNORE_RETURNING);
+ /*
+ * Returning clause on the relation being replaced with row-
+ * security subquery shall be handled in a special way, because
+ * of no system columns on subquery.
+ * Var references to system column or whole-row reference need
+ * to be adjusted to reference artificial columns on behalf of
+ * the underlying these columns, however, RETURNGIN clause is
+ * an exception because its Var nodes are evaluated towards
+ * the "raw" target relation, not a fetched tuple.
+ */
+ context.in_returning = true;
+ newnode->returningList = (List *)
+ expression_tree_mutator((Node *) newnode->returningList,
+ adjust_appendrel_attrs_mutator,
+ (void *) &context);
if (newnode->resultRelation == appinfo->parent_relid)
{
- newnode->resultRelation = appinfo->child_relid;
+ newnode->resultRelation = (appinfo->child_result > 0 ?
+ appinfo->child_result :
+ appinfo->child_relid);
+ newnode->sourceRelation = appinfo->child_relid;
/* Fix tlist resnos too, if it's inherited UPDATE */
if (newnode->commandType == CMD_UPDATE)
newnode->targetList =
@@ -1624,6 +1645,49 @@ adjust_appendrel_attrs(PlannerInfo *root, Node *node, AppendRelInfo *appinfo)
}
static Node *
+fixup_var_on_rowsec_subquery(RangeTblEntry *rte, Var *var)
+{
+ ListCell *cell;
+
+ Assert(rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_SECURITY);
+ /*
+ * In case when row-level security policy is applied on the referenced
+ * table, its RangeTblEntry (RTE_RELATION) is replaced with sub-query
+ * to filter out unprivileged rows of underlying relation.
+ * Even though reference to this sub-query should perform as if ones
+ * to real relations, system column has to be cared in special way
+ * due to the nature of sub-query.
+ * Target-entries that reference system columns should be added on
+ * rowlevelsec.c, so all we need to do here is looking up underlying
+ * target-list that can reference underlying system column, and fix-
+ * up varattno of the referencing Var node with resno of TargetEntry.
+ */
+ foreach (cell, rte->subquery->targetList)
+ {
+ TargetEntry *subtle = lfirst(cell);
+
+ if (IsA(subtle->expr, Var))
+ {
+ Var *subvar = (Var *) subtle->expr;
+ Var *newnode;
+
+ if (subvar->varattno == var->varattno)
+ {
+ newnode = copyObject(var);
+ newnode->varattno = subtle->resno;
+ return (Node *)newnode;
+ }
+ }
+ else
+ Assert(IsA(subtle->expr, Const));
+ }
+ elog(ERROR, "could not find artificial column of %d, relid %u",
+ var->varattno, var->varno);
+ return NULL;
+}
+
+static Node *
adjust_appendrel_attrs_mutator(Node *node,
adjust_appendrel_attrs_context *context)
{
@@ -1638,8 +1702,12 @@ adjust_appendrel_attrs_mutator(Node *node,
if (var->varlevelsup == 0 &&
var->varno == appinfo->parent_relid)
{
- var->varno = appinfo->child_relid;
+ var->varno = (context->in_returning &&
+ appinfo->child_result > 0 ?
+ appinfo->child_result :
+ appinfo->child_relid);
var->varnoold = appinfo->child_relid;
+
if (var->varattno > 0)
{
Node *newnode;
@@ -1664,6 +1732,14 @@ adjust_appendrel_attrs_mutator(Node *node,
*/
if (OidIsValid(appinfo->child_reltype))
{
+ Query *parse = context->root->parse;
+ RangeTblEntry *rte = rt_fetch(appinfo->child_relid,
+ parse->rtable);
+ if (!context->in_returning &&
+ rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_SECURITY)
+ var = (Var *)fixup_var_on_rowsec_subquery(rte, var);
+
Assert(var->vartype == appinfo->parent_reltype);
if (appinfo->parent_reltype != appinfo->child_reltype)
{
@@ -1708,7 +1784,18 @@ adjust_appendrel_attrs_mutator(Node *node,
return (Node *) rowexpr;
}
}
- /* system attributes don't need any other translation */
+ else
+ {
+ Query *parse = context->root->parse;
+ RangeTblEntry *rte;
+
+ rte = rt_fetch(appinfo->child_relid, parse->rtable);
+
+ if (!context->in_returning &&
+ rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_SECURITY)
+ return fixup_var_on_rowsec_subquery(rte, var);
+ }
}
return (Node *) var;
}
diff --git a/src/backend/optimizer/util/Makefile b/src/backend/optimizer/util/Makefile
index 3b2d16b..3f5cb19 100644
--- a/src/backend/optimizer/util/Makefile
+++ b/src/backend/optimizer/util/Makefile
@@ -13,6 +13,6 @@ top_builddir = ../../../..
include $(top_builddir)/src/Makefile.global
OBJS = clauses.o joininfo.o pathnode.o placeholder.o plancat.o predtest.o \
- relnode.o restrictinfo.o tlist.o var.o
+ relnode.o restrictinfo.o tlist.o var.o rowsecurity.o
include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/optimizer/util/rowsecurity.c b/src/backend/optimizer/util/rowsecurity.c
new file mode 100644
index 0000000..d686dfa
--- /dev/null
+++ b/src/backend/optimizer/util/rowsecurity.c
@@ -0,0 +1,733 @@
+/*
+ * optimizer/util/rowsecurity.c
+ * Routines to support row-security feature
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ */
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "access/htup_details.h"
+#include "access/sysattr.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_inherits_fn.h"
+#include "catalog/pg_rowsecurity.h"
+#include "catalog/pg_type.h"
+#include "miscadmin.h"
+#include "nodes/makefuncs.h"
+#include "nodes/nodeFuncs.h"
+#include "nodes/plannodes.h"
+#include "optimizer/clauses.h"
+#include "optimizer/prep.h"
+#include "optimizer/rowsecurity.h"
+#include "parser/parsetree.h"
+#include "rewrite/rewriteHandler.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "tcop/utility.h"
+
+/* flags to pull row-security policy */
+#define RSEC_FLAG_HAS_SUBLINKS 0x0001
+
+/* hook to allow extensions to apply their own security policy */
+row_security_policy_hook_type row_security_policy_hook = NULL;
+
+/*
+ * make_artificial_column
+ *
+ * It makes a target-entry node that references underlying column.
+ * Its tle->expr is usualy Var node, but may be Const for dummy NULL
+ * if the supplied attribute was already dropped.
+ */
+static TargetEntry *
+make_artificial_column(RangeTblEntry *subrte, AttrNumber attnum)
+{
+ Expr *expr;
+ char *resname;
+
+ Assert(subrte->rtekind == RTE_RELATION && OidIsValid(subrte->relid));
+ if (attnum == InvalidAttrNumber)
+ {
+ expr = (Expr *) makeWholeRowVar(subrte, (Index) 1, 0, false);
+ resname = get_rel_name(subrte->relid);
+ }
+ else
+ {
+ HeapTuple tuple;
+ Form_pg_attribute attform;
+
+ tuple = SearchSysCache2(ATTNUM,
+ ObjectIdGetDatum(subrte->relid),
+ Int16GetDatum(attnum));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for attribute %d of relation %u",
+ attnum, subrte->relid);
+ attform = (Form_pg_attribute) GETSTRUCT(tuple);
+
+ if (attform->attisdropped)
+ {
+ char namebuf[NAMEDATALEN];
+
+ /* Insert NULL just for a placeholder of dropped column */
+ expr = (Expr *) makeConst(INT4OID,
+ -1,
+ InvalidOid,
+ sizeof(int32),
+ (Datum) 0,
+ true, /* isnull */
+ true); /* byval */
+ sprintf(namebuf, "dummy-%d", (int)attform->attnum);
+ resname = pstrdup(namebuf);
+ }
+ else
+ {
+ expr = (Expr *) makeVar((Index) 1,
+ attform->attnum,
+ attform->atttypid,
+ attform->atttypmod,
+ attform->attcollation,
+ 0);
+ resname = pstrdup(NameStr(attform->attname));
+ }
+ ReleaseSysCache(tuple);
+ }
+ return makeTargetEntry(expr, -1, resname, false);
+}
+
+/*
+ * lookup_artificial_column
+ *
+ * It looks-up resource number of the target-entry relevant to the given
+ * Var-node that references the row-security subquery. If required column
+ * is not in the subquery's target-list, this function also adds new one
+ * and returns its resource number.
+ */
+static AttrNumber
+lookup_artificial_column(PlannerInfo *root,
+ RangeTblEntry *rte, AttrNumber varattno)
+{
+ Query *subqry;
+ RangeTblEntry *subrte;
+ TargetEntry *subtle;
+ ListCell *cell;
+
+ Assert(rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_SECURITY);
+
+ subqry = rte->subquery;
+ foreach (cell, subqry->targetList)
+ {
+ subtle = lfirst(cell);
+
+ /*
+ * If referenced artifical column is already constructed on the
+ * target-list of row-security subquery, nothing to do any more.
+ */
+ if (IsA(subtle->expr, Var))
+ {
+ Var *subvar = (Var *)subtle->expr;
+
+ Assert(subvar->varno == 1);
+ if (subvar->varattno == varattno)
+ return subtle->resno;
+ }
+ }
+
+ /*
+ * OK, we don't have an artifical column relevant to the required ones,
+ * so let's create a new artifical column on demand.
+ */
+ subrte = rt_fetch((Index) 1, subqry->rtable);
+ subtle = make_artificial_column(subrte, varattno);
+ subtle->resno = list_length(subqry->targetList) + 1;
+
+ subqry->targetList = lappend(subqry->targetList, subtle);
+ rte->eref->colnames = lappend(rte->eref->colnames,
+ makeString(pstrdup(subtle->resname)));
+ return subtle->resno;
+}
+
+/*
+ * fixup_varnode_walker
+ *
+ * It recursively fixes up references to the relation to be replaced by
+ * row-security sub-query, and adds artificial columns relevant to the
+ * underlying system columns or whole row-reference on demand.
+ */
+typedef struct {
+ PlannerInfo *root;
+ int varlevelsup;
+ Index *vartrans;
+} fixup_varnode_context;
+
+static bool
+fixup_varnode_walker(Node *node, fixup_varnode_context *context)
+{
+ if (node == NULL)
+ return false;
+
+ if (IsA(node, Var))
+ {
+ Var *var = (Var *) node;
+ List *rtable = context->root->parse->rtable;
+ RangeTblEntry *rte;
+ ListCell *cell;
+
+ /*
+ * Ignore it, if Var node does not reference the Query currently
+ * we focues on.
+ */
+ if (var->varlevelsup != context->varlevelsup)
+ return false;
+
+ /*
+ * Var nodes that reference the relation being replaced by row-
+ * security sub-query has to be adjusted; to reference the sub-
+ * query, instead of the original relation.
+ */
+ if (context->vartrans[var->varno] != 0)
+ {
+ rte = rt_fetch(context->vartrans[var->varno], rtable);
+ if (rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_SECURITY)
+ {
+ var->varno = var->varnoold = context->vartrans[var->varno];
+ var->varattno = lookup_artificial_column(context->root,
+ rte, var->varattno);
+ }
+ }
+ else
+ {
+ rte = rt_fetch(var->varno, rtable);
+ if (!rte->inh)
+ return false;
+
+ foreach (cell, context->root->append_rel_list)
+ {
+ AppendRelInfo *appinfo = lfirst(cell);
+ RangeTblEntry *child_rte;
+
+ if (appinfo->parent_relid != var->varno)
+ continue;
+
+ if (var->varattno > InvalidAttrNumber)
+ continue;
+
+ child_rte = rt_fetch(appinfo->child_relid, rtable);
+ if (child_rte->rtekind == RTE_SUBQUERY &&
+ child_rte->subquery->querySource == QSRC_ROW_SECURITY)
+ (void) lookup_artificial_column(context->root,
+ child_rte,
+ var->varattno);
+ }
+ }
+ }
+ else if (IsA(node, RangeTblRef))
+ {
+ RangeTblRef *rtr = (RangeTblRef *) node;
+
+ if (context->varlevelsup == 0 &&
+ context->vartrans[rtr->rtindex] != 0)
+ rtr->rtindex = context->vartrans[rtr->rtindex];
+ }
+ else if (IsA(node, Query))
+ {
+ bool result;
+
+ context->varlevelsup++;
+ result = query_tree_walker((Query *) node,
+ fixup_varnode_walker,
+ (void *) context, 0);
+ context->varlevelsup--;
+
+ return result;
+ }
+ return expression_tree_walker(node,
+ fixup_varnode_walker,
+ (void *) context);
+}
+
+/*
+ * check_infinite_recursion
+ *
+ * It is a wrong row-security configuration, if we try to expand
+ * the relation inside of row-security subquery originated from
+ * same relation!
+ */
+static void
+check_infinite_recursion(PlannerInfo *root, Oid relid)
+{
+ PlannerInfo *parent = root->parent_root;
+
+ if (parent && parent->parse->querySource == QSRC_ROW_SECURITY)
+ {
+ RangeTblEntry *rte = rt_fetch(1, parent->parse->rtable);
+
+ Assert(rte->rtekind == RTE_RELATION && OidIsValid(rte->relid));
+
+ if (relid == rte->relid)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("infinite recursion detected for relation \"%s\"",
+ get_rel_name(relid))));
+ check_infinite_recursion(parent, relid);
+ }
+}
+
+/*
+ * expand_rtentry_with_policy
+ *
+ * It extends a range-table entry of row-security sub-query with supplied
+ * security policy, and append it on the parse->rtable.
+ * This sub-query contains artificial columns that reference underlying
+ * regular columns (at least, references to system column or whole of
+ * table reference shall be added on demand), and simple scan on the
+ * target relation.
+ * Any Var nodes that referenced the relation pointed by rtindex shall
+ * be adjusted to reference this sub-query instead. walker
+ *
+ */
+static Index
+expand_rtentry_with_policy(PlannerInfo *root, Index rtindex,
+ Expr *qual, int flags)
+{
+ Query *parse = root->parse;
+ RangeTblEntry *rte = rt_fetch(rtindex, parse->rtable);
+ Query *subqry;
+ RangeTblEntry *subrte;
+ RangeTblRef *subrtr;
+ TargetEntry *subtle;
+ RangeTblEntry *newrte;
+ HeapTuple tuple;
+ AttrNumber nattrs;
+ AttrNumber attnum;
+ List *targetList = NIL;
+ List *colNameList = NIL;
+ PlanRowMark *rowmark;
+
+ Assert(rte->rtekind == RTE_RELATION && !rte->inh);
+
+ /* check recursion to prevent infinite loop */
+ check_infinite_recursion(root, rte->relid);
+
+ /* Expand views inside SubLink node */
+ if (flags & RSEC_FLAG_HAS_SUBLINKS)
+ QueryRewriteExpr((Node *)qual, list_make1_oid(rte->relid));
+
+ /*
+ * Construction of sub-query
+ */
+ subqry = (Query *) makeNode(Query);
+ subqry->commandType = CMD_SELECT;
+ subqry->querySource = QSRC_ROW_SECURITY;
+
+ subrte = copyObject(rte);
+ subqry->rtable = list_make1(subrte);
+
+ subrtr = makeNode(RangeTblRef);
+ subrtr->rtindex = 1;
+ subqry->jointree = makeFromExpr(list_make1(subrtr), (Node *) qual);
+ if (flags & RSEC_FLAG_HAS_SUBLINKS)
+ subqry->hasSubLinks = true;
+
+ /*
+ * Construction of TargetEntries that reference underlying columns.
+ */
+ tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(rte->relid));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for relation %u", rte->relid);
+ nattrs = ((Form_pg_class) GETSTRUCT(tuple))->relnatts;
+ ReleaseSysCache(tuple);
+
+ for (attnum = 1; attnum <= nattrs; attnum++)
+ {
+ subtle = make_artificial_column(subrte, attnum);
+ subtle->resno = list_length(targetList) + 1;
+ Assert(subtle->resno == attnum);
+
+ targetList = lappend(targetList, subtle);
+ colNameList = lappend(colNameList,
+ makeString(pstrdup(subtle->resname)));
+ }
+ subqry->targetList = targetList;
+
+ /* Expand RengeTblEntry with this sub-query */
+ newrte = makeNode(RangeTblEntry);
+ newrte->rtekind = RTE_SUBQUERY;
+ newrte->subquery = subqry;
+ newrte->security_barrier = true;
+ newrte->rowsec_relid = rte->relid;
+ newrte->eref = makeAlias(get_rel_name(rte->relid), colNameList);
+
+ parse->rtable = lappend(parse->rtable, newrte);
+
+ /* Push-down rowmark, if needed */
+ rowmark = get_plan_rowmark(root->rowMarks, rtindex);
+ if (rowmark)
+ {
+ /*
+ * In case of inherited children, rti/prti of rowmark shall be
+ * fixed up later, on inheritance_planner().
+ */
+ if (rowmark->rti == rowmark->prti)
+ rowmark->rti = rowmark->prti = list_length(parse->rtable);
+ else
+ rowmark->rti = list_length(parse->rtable);
+
+ lookup_artificial_column(root, newrte, SelfItemPointerAttributeNumber);
+ lookup_artificial_column(root, newrte, TableOidAttributeNumber);
+ }
+
+ return list_length(parse->rtable);
+}
+
+/*
+ * pull_row_security_policy
+ *
+ * It pulls the configured row-security policy of both built-in and
+ * extensions. If any, it returns expression tree.
+ */
+static Expr *
+pull_row_security_policy(CmdType cmd, Relation relation, int *p_flags)
+{
+ Expr *quals = NULL;
+ int flags = 0;
+
+ /*
+ * Pull the row-security policy configured with built-in features,
+ * if unprivileged users. Please note that superuser can bypass it.
+ */
+ if (relation->rsdesc && !superuser())
+ {
+ RowSecurityDesc *rsdesc = relation->rsdesc;
+
+ quals = copyObject(rsdesc->rsall.qual);
+ if (rsdesc->rsall.hassublinks)
+ flags |= RSEC_FLAG_HAS_SUBLINKS;
+ }
+
+ /*
+ * Also, ask extensions whether they want to apply their own
+ * row-security policy. If both built-in and extension has
+ * their own policy, it shall be merged.
+ */
+ if (row_security_policy_hook)
+ {
+ List *temp;
+
+ temp = (*row_security_policy_hook)(cmd, relation);
+ if (temp != NIL)
+ {
+ if ((flags & RSEC_FLAG_HAS_SUBLINKS) == 0 &&
+ contain_subplans((Node *) temp))
+ flags |= RSEC_FLAG_HAS_SUBLINKS;
+
+ if (quals != NULL)
+ temp = lappend(temp, quals);
+
+ if (list_length(temp) == 1)
+ quals = (Expr *)list_head(temp);
+ else if (list_length(temp) > 1)
+ quals = makeBoolExpr(AND_EXPR, temp, -1);
+ }
+ }
+ *p_flags = flags;
+ return quals;
+}
+
+/*
+ * copy_row_security_policy
+ *
+ * It construct a row-security subquery instead of raw COPY TO statement,
+ * if target relation has a row-level security policy
+ */
+bool
+copy_row_security_policy(CopyStmt *stmt, Relation rel, List *attnums)
+{
+ Expr *quals;
+ int flags;
+ Query *parse;
+ RangeTblEntry *rte;
+ RangeTblRef *rtr;
+ TargetEntry *tle;
+ Var *var;
+ ListCell *cell;
+
+ if (stmt->is_from)
+ return false;
+
+ quals = pull_row_security_policy(CMD_SELECT, rel, &flags);
+ if (!quals)
+ return false;
+
+ parse = (Query *) makeNode(Query);
+ parse->commandType = CMD_SELECT;
+ parse->querySource = QSRC_ROW_SECURITY;
+
+ rte = makeNode(RangeTblEntry);
+ rte->rtekind = RTE_RELATION;
+ rte->relid = RelationGetRelid(rel);
+ rte->relkind = RelationGetForm(rel)->relkind;
+
+ foreach (cell, attnums)
+ {
+ HeapTuple tuple;
+ Form_pg_attribute attform;
+ AttrNumber attno = lfirst_int(cell);
+
+ tuple = SearchSysCache2(ATTNUM,
+ ObjectIdGetDatum(RelationGetRelid(rel)),
+ Int16GetDatum(attno));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for attribute %d of relation %s",
+ attno, RelationGetRelationName(rel));
+ attform = (Form_pg_attribute) GETSTRUCT(tuple);
+
+ var = makeVar((Index) 1,
+ attform->attnum,
+ attform->atttypid,
+ attform->atttypmod,
+ attform->attcollation,
+ 0);
+ tle = makeTargetEntry((Expr *) var,
+ list_length(parse->targetList) + 1,
+ pstrdup(NameStr(attform->attname)),
+ false);
+ parse->targetList = lappend(parse->targetList, tle);
+
+ ReleaseSysCache(tuple);
+
+ rte->selectedCols = bms_add_member(rte->selectedCols,
+ attno - FirstLowInvalidHeapAttributeNumber);
+ }
+ rte->inFromCl = true;
+ rte->requiredPerms = ACL_SELECT;
+
+ rtr = makeNode(RangeTblRef);
+ rtr->rtindex = 1;
+
+ parse->jointree = makeFromExpr(list_make1(rtr), (Node *) quals);
+ parse->rtable = list_make1(rte);
+ if (flags & RSEC_FLAG_HAS_SUBLINKS)
+ parse->hasSubLinks = true;
+
+ stmt->query = (Node *) parse;
+
+ return true;
+}
+
+/*
+ * apply_row_security_relation
+ *
+ * It applies row-security policy on a particular relation being specified.
+ * If this relation is top of the inheritance tree, it also checks inherited
+ * children.
+ */
+static bool
+apply_row_security_relation(PlannerInfo *root, Index *vartrans,
+ CmdType cmd, Index rtindex)
+{
+ Query *parse = root->parse;
+ RangeTblEntry *rte = rt_fetch(rtindex, parse->rtable);
+ bool result = false;
+
+ if (!rte->inh)
+ {
+ Relation rel;
+ Expr *qual;
+ int flags = 0;
+
+ rel = heap_open(rte->relid, NoLock);
+ qual = pull_row_security_policy(cmd, rel, &flags);
+ if (qual)
+ {
+ vartrans[rtindex]
+ = expand_rtentry_with_policy(root, rtindex, qual, flags);
+ if (parse->resultRelation == rtindex)
+ parse->sourceRelation = vartrans[rtindex];
+ result = true;
+ }
+ heap_close(rel, NoLock);
+ }
+ else
+ {
+ ListCell *lc1, *lc2;
+
+ foreach (lc1, root->append_rel_list)
+ {
+ AppendRelInfo *apinfo = lfirst(lc1);
+
+ if (apinfo->parent_relid != rtindex)
+ continue;
+
+ if (apply_row_security_relation(root, vartrans, cmd,
+ apinfo->child_relid))
+ {
+ if (parse->resultRelation == rtindex)
+ apinfo->child_result = apinfo->child_relid;
+ apinfo->child_relid = vartrans[apinfo->child_relid];
+ foreach (lc2, apinfo->translated_vars)
+ {
+ Var *var = lfirst(lc2);
+
+ if (var)
+ var->varno = apinfo->child_relid;
+ }
+ result = true;
+ }
+ }
+ }
+ return result;
+}
+
+/*
+ * apply_row_security_recursive
+ *
+ * walker on join-tree
+ */
+static bool
+apply_row_security_recursive(PlannerInfo *root, Index *vartrans, Node *jtnode)
+{
+ bool result = false;
+
+ if (jtnode == NULL)
+ return false;
+ if (IsA(jtnode, RangeTblRef))
+ {
+ Index rtindex = ((RangeTblRef *) jtnode)->rtindex;
+ Query *parse = root->parse;
+ RangeTblEntry *rte = rt_fetch(rtindex, parse->rtable);
+ CmdType cmd;
+
+ /* Only relation can have row-security policy */
+ if (rte->rtekind != RTE_RELATION)
+ return false;
+
+ /*
+ * Prevents infinite recursion. Please note that rtindex == 1
+ * of the row-security subquery is a relation being already
+ * processed on the upper level.
+ */
+ if (parse->querySource == QSRC_ROW_SECURITY && rtindex == 1)
+ return false;
+
+ /* Is it a result relation of UPDATE or DELETE command? */
+ if (parse->resultRelation == rtindex)
+ cmd = parse->commandType;
+ else
+ cmd = CMD_SELECT;
+
+ /* Try to apply row-security policy, if configured */
+ result = apply_row_security_relation(root, vartrans, cmd, rtindex);
+ }
+ else if (IsA(jtnode, FromExpr))
+ {
+ FromExpr *f = (FromExpr *) jtnode;
+ ListCell *l;
+
+ foreach (l, f->fromlist)
+ {
+ if (apply_row_security_recursive(root, vartrans, lfirst(l)))
+ result = true;
+ }
+ }
+ else if (IsA(jtnode, JoinExpr))
+ {
+ JoinExpr *j = (JoinExpr *) jtnode;
+
+ if (apply_row_security_recursive(root, vartrans, j->larg))
+ result = true;
+ if (apply_row_security_recursive(root, vartrans, j->rarg))
+ result = true;
+ }
+ else
+ elog(ERROR, "unexpected node type: %d", (int) nodeTag(jtnode));
+
+ return result;
+}
+
+/*
+ * apply_row_security_policy
+ *
+ * Entrypoint to apply configured row-security policy of the relation.
+ *
+ * In case when the supplied query references relations with row-security
+ * policy, its RangeTblEntry shall be replaced by a row-security subquery
+ * that has simple scan on the referenced table with policy qualifiers.
+ * Of course, security-barrier shall be set on the subquery to prevent
+ * unexpected push-down of functions without leakproof flag.
+ *
+ * For example, when table t1 has a security policy "(x % 2 = 0)", the
+ * following query:
+ * SELECT * FROM t1 WHERE f_leak(y)
+ * performs as if
+ * SELECT * FROM (
+ * SELECT x, y FROM t1 WHERE (x % 2 = 0)
+ * ) AS t1 WHERE f_leak(y)
+ * would be given. Because the sub-query has security barrier flag,
+ * configured security policy qualifier is always executed prior to
+ * user given functions.
+ */
+void
+apply_row_security_policy(PlannerInfo *root)
+{
+ Query *parse = root->parse;
+ Oid curr_userid;
+ int curr_seccxt;
+ Index *vartrans;
+
+ /*
+ * Mode checks. In case when SECURITY_ROW_LEVEL_DISABLED is set,
+ * no row-level security policy should be applied regardless
+ * whether it is built-in or extension.
+ */
+ GetUserIdAndSecContext(&curr_userid, &curr_seccxt);
+ if (curr_seccxt & SECURITY_ROW_LEVEL_DISABLED)
+ return;
+
+ vartrans = palloc0(sizeof(Index) * (list_length(parse->rtable) + 1));
+ if (apply_row_security_recursive(root, vartrans, (Node *)parse->jointree))
+ {
+ PlannerGlobal *glob = root->glob;
+ PlanInvalItem *pi_item;
+ fixup_varnode_context context;
+
+ /*
+ * Constructed Plan with row-level security policy depends on
+ * properties of current user (database superuser can bypass
+ * configured row-security policy!), thus, it has to be
+ * invalidated when its assumption was changed.
+ */
+ if (!OidIsValid(glob->planUserId))
+ {
+ /* Plan invalidation on session user-id */
+ glob->planUserId = GetUserId();
+
+ /* Plan invalidation on catalog updates of pg_authid */
+ pi_item = makeNode(PlanInvalItem);
+ pi_item->cacheId = AUTHOID;
+ pi_item->hashValue =
+ GetSysCacheHashValue1(AUTHOID,
+ ObjectIdGetDatum(glob->planUserId));
+ glob->invalItems = lappend(glob->invalItems, pi_item);
+ }
+ else
+ Assert(glob->planUserId == GetUserId());
+
+ /*
+ * Var-nodes that referenced RangeTblEntry to be replaced by
+ * row-security sub-query have to be adjusted for appropriate
+ * reference to the underlying artificial column of the relation.
+ */
+ context.root = root;
+ context.varlevelsup = 0;
+ context.vartrans = vartrans;
+ query_tree_walker(parse,
+ fixup_varnode_walker,
+ (void *) &context,
+ QTW_IGNORE_RETURNING);
+ }
+ pfree(vartrans);
+}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 5094226..ba79767 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -256,6 +256,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
%type <node> alter_table_cmd alter_type_cmd opt_collate_clause
%type <list> alter_table_cmds alter_type_cmds
+%type <str> row_security_cmd
%type <dbehavior> opt_drop_behavior
@@ -2156,6 +2157,24 @@ alter_table_cmd:
n->def = (Node *)$2;
$$ = (Node *)n;
}
+ /* ALTER TABLE <name> SET ROW SECURITY FOR <cmd> TO (<expr>) */
+ | SET ROW SECURITY FOR row_security_cmd TO '(' a_expr ')'
+ {
+ AlterTableCmd *n = makeNode(AlterTableCmd);
+ n->subtype = AT_SetRowSecurity;
+ n->name = $5;
+ n->def = (Node *) $8;
+ $$ = (Node *)n;
+ }
+ /* ALTER TABLE <name> RESET ROW SECURITY FOR <cmd> */
+ | RESET ROW SECURITY FOR row_security_cmd
+ {
+ AlterTableCmd *n = makeNode(AlterTableCmd);
+ n->subtype = AT_ResetRowSecurity;
+ n->name = $5;
+ n->def = NULL;
+ $$ = (Node *)n;
+ }
| alter_generic_options
{
AlterTableCmd *n = makeNode(AlterTableCmd);
@@ -2227,6 +2246,12 @@ reloption_elem:
}
;
+row_security_cmd: ALL { $$ = "all"; }
+ | SELECT { $$ = "select"; }
+ | INSERT { $$ = "insert"; }
+ | UPDATE { $$ = "update"; }
+ | DELETE_P { $$ = "delete"; }
+ ;
/*****************************************************************************
*
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
index 7380618..14e02d4 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -269,6 +269,9 @@ transformAggregateCall(ParseState *pstate, Aggref *agg,
case EXPR_KIND_TRIGGER_WHEN:
err = _("aggregate functions are not allowed in trigger WHEN conditions");
break;
+ case EXPR_KIND_ROW_SECURITY:
+ err = _("aggregate functions are not allowed in row-security policy");
+ break;
/*
* There is intentionally no default: case here, so that the
@@ -537,6 +540,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
case EXPR_KIND_TRIGGER_WHEN:
err = _("window functions are not allowed in trigger WHEN conditions");
break;
+ case EXPR_KIND_ROW_SECURITY:
+ err = _("window functions are not allowed in row-security policy");
+ break;
/*
* There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 7f0995f..3a48edb 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -1445,6 +1445,7 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
case EXPR_KIND_OFFSET:
case EXPR_KIND_RETURNING:
case EXPR_KIND_VALUES:
+ case EXPR_KIND_ROW_SECURITY:
/* okay */
break;
case EXPR_KIND_CHECK_CONSTRAINT:
@@ -2624,6 +2625,8 @@ ParseExprKindName(ParseExprKind exprKind)
return "EXECUTE";
case EXPR_KIND_TRIGGER_WHEN:
return "WHEN";
+ case EXPR_KIND_ROW_SECURITY:
+ return "ROW SECURITY";
/*
* There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c
index a9254c8..768d531 100644
--- a/src/backend/parser/parse_relation.c
+++ b/src/backend/parser/parse_relation.c
@@ -2058,6 +2058,25 @@ expandRelAttrs(ParseState *pstate, RangeTblEntry *rte,
}
/*
+ * getrelid
+ *
+ * Get OID of the relation corresponding to the given range index.
+ * Note that InvalidOid will be returned if the RTE is for neither
+ * relation nor sub-query originated from a relation
+ */
+Oid
+getrelid(Index rangeindex, List *rangetable)
+{
+ RangeTblEntry *rte = rt_fetch(rangeindex, rangetable);
+
+ if (rte->rtekind == RTE_RELATION)
+ return rte->relid;
+ if (rte->rtekind == RTE_SUBQUERY)
+ return rte->rowsec_relid;
+ return InvalidOid;
+}
+
+/*
* get_rte_attribute_name
* Get an attribute name from a RangeTblEntry
*
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index 01875fc..abfd158 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -2941,3 +2941,19 @@ QueryRewrite(Query *parsetree)
return results;
}
+
+/*
+ * QueryRewriteExpr
+ *
+ * This routine provides an entry point of query rewriter towards
+ * a certain expression tree with SubLink node; being added after
+ * the top level query rewrite.
+ * It primarily intends to expand views appeared in the qualifiers
+ * appended with row-level security which needs to modify query
+ * tree at head of the planner stage.
+ */
+void
+QueryRewriteExpr(Node *node, List *activeRIRs)
+{
+ fireRIRonSubLink(node, activeRIRs);
+}
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index 65edc1f..b36da97 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -3008,6 +3008,7 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
int spi_result;
Oid save_userid;
int save_sec_context;
+ int temp_sec_context;
Datum vals[RI_MAX_NUMKEYS * 2];
char nulls[RI_MAX_NUMKEYS * 2];
@@ -3087,8 +3088,18 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
/* Switch to proper UID to perform check as */
GetUserIdAndSecContext(&save_userid, &save_sec_context);
+
+ /*
+ * Row-level security should be disabled in case when foreign-key
+ * relation is queried to check existence of tuples that references
+ * the primary-key being modified.
+ */
+ temp_sec_context = save_sec_context | SECURITY_LOCAL_USERID_CHANGE;
+ if (source_is_pk)
+ temp_sec_context |= SECURITY_ROW_LEVEL_DISABLED;
+
SetUserIdAndSecContext(RelationGetForm(query_rel)->relowner,
- save_sec_context | SECURITY_LOCAL_USERID_CHANGE);
+ temp_sec_context);
/* Finally we can run the query. */
spi_result = SPI_execute_snapshot(qplan,
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index 26cae97..d05cde7 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -53,6 +53,7 @@
#include "catalog/namespace.h"
#include "executor/executor.h"
#include "executor/spi.h"
+#include "miscadmin.h"
#include "nodes/nodeFuncs.h"
#include "optimizer/planmain.h"
#include "optimizer/prep.h"
@@ -794,6 +795,16 @@ CheckCachedPlan(CachedPlanSource *plansource)
AcquireExecutorLocks(plan->stmt_list, true);
/*
+ * If plan was constructed with assumption of a particular user-id,
+ * and it is different from the current one, the cached-plan shall
+ * be invalidated to construct suitable query plan.
+ */
+ if (plan->is_valid &&
+ OidIsValid(plan->planUserId) &&
+ plan->planUserId == GetUserId())
+ plan->is_valid = false;
+
+ /*
* If plan was transient, check to see if TransactionXmin has
* advanced, and if so invalidate it.
*/
@@ -846,6 +857,8 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
{
CachedPlan *plan;
List *plist;
+ ListCell *cell;
+ Oid planUserId = InvalidOid;
bool snapshot_set;
bool spi_pushed;
MemoryContext plan_context;
@@ -913,6 +926,24 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
PopActiveSnapshot();
/*
+ * Check whether the generated plan assumes a particular user-id, or not.
+ * In case when a valid user-id is recorded on PlannedStmt->planUserId,
+ * it should be kept and used to validation check of the cached plan
+ * under the "current" user-id.
+ */
+ foreach (cell, plist)
+ {
+ PlannedStmt *pstmt = lfirst(cell);
+
+ if (IsA(pstmt, PlannedStmt) && OidIsValid(pstmt->planUserId))
+ {
+ Assert(!OidIsValid(planUserId) || planUserId == pstmt->planUserId);
+
+ planUserId = pstmt->planUserId;
+ }
+ }
+
+ /*
* Normally we make a dedicated memory context for the CachedPlan and its
* subsidiary data. (It's probably not going to be large, but just in
* case, use the default maxsize parameter. It's transient for the
@@ -955,6 +986,7 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
plan->is_oneshot = plansource->is_oneshot;
plan->is_saved = false;
plan->is_valid = true;
+ plan->planUserId = planUserId;
/* assign generation number to new plan */
plan->generation = ++(plansource->generation);
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index f114038..39bfeaf 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -50,6 +50,7 @@
#include "catalog/pg_opclass.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_rewrite.h"
+#include "catalog/pg_rowsecurity.h"
#include "catalog/pg_tablespace.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
@@ -933,6 +934,11 @@ RelationBuildDesc(Oid targetRelId, bool insertIt)
else
relation->trigdesc = NULL;
+ if (relation->rd_rel->relhasrowsecurity)
+ RelationBuildRowSecurity(relation);
+ else
+ relation->rsdesc = NULL;
+
/*
* if it's an index, initialize index-related information
*/
@@ -1840,6 +1846,8 @@ RelationDestroyRelation(Relation relation)
MemoryContextDelete(relation->rd_indexcxt);
if (relation->rd_rulescxt)
MemoryContextDelete(relation->rd_rulescxt);
+ if (relation->rsdesc)
+ MemoryContextDelete(relation->rsdesc->rscxt);
if (relation->rd_fdwroutine)
pfree(relation->rd_fdwroutine);
pfree(relation);
@@ -3159,7 +3167,13 @@ RelationCacheInitializePhase3(void)
relation->rd_rel->relhastriggers = false;
restart = true;
}
-
+ if (relation->rd_rel->relhasrowsecurity && relation->rsdesc == NULL)
+ {
+ RelationBuildRowSecurity(relation);
+ if (relation->rsdesc == NULL)
+ relation->rd_rel->relhasrowsecurity = false;
+ restart = true;
+ }
/* Release hold on the relation */
RelationDecrementReferenceCount(relation);
@@ -4405,6 +4419,7 @@ load_relcache_init_file(bool shared)
rel->rd_rules = NULL;
rel->rd_rulescxt = NULL;
rel->trigdesc = NULL;
+ rel->rsdesc = NULL;
rel->rd_indexprs = NIL;
rel->rd_indpred = NIL;
rel->rd_exclops = NULL;
diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c
index ae52ac1..cd1bed1 100644
--- a/src/bin/pg_dump/common.c
+++ b/src/bin/pg_dump/common.c
@@ -244,6 +244,10 @@ getSchemaData(Archive *fout, int *numTablesPtr)
write_msg(NULL, "reading event triggers\n");
getEventTriggers(fout, &numEventTriggers);
+ if (g_verbose)
+ write_msg(NULL, "reading row-security policies\n");
+ getRowSecurity(fout, tblinfo, numTables);
+
*numTablesPtr = numTables;
return tblinfo;
}
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index ec956ad..9a9dd6b 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -249,6 +249,7 @@ static char *myFormatType(const char *typname, int32 typmod);
static void getBlobs(Archive *fout);
static void dumpBlob(Archive *fout, BlobInfo *binfo);
static int dumpBlobs(Archive *fout, void *arg);
+static void dumpRowSecurity(Archive *fout, RowSecurityInfo *rsinfo);
static void dumpDatabase(Archive *AH);
static void dumpEncoding(Archive *AH);
static void dumpStdStrings(Archive *AH);
@@ -2686,6 +2687,130 @@ dumpBlobs(Archive *fout, void *arg)
return 1;
}
+/*
+ * getRowSecurity
+ * get information about every row-security policy on a dumpable table
+ */
+void
+getRowSecurity(Archive *fout, TableInfo tblinfo[], int numTables)
+{
+ PQExpBuffer query = createPQExpBuffer();
+ PGresult *res;
+ RowSecurityInfo *rsinfo;
+ int i_oid;
+ int i_tableoid;
+ int i_rseccmd;
+ int i_rsecqual;
+ int i, j, ntups;
+
+ for (i=0; i < numTables; i++)
+ {
+ TableInfo *tbinfo = &tblinfo[i];
+
+ if (!tbinfo->hasrowsec || !tbinfo->dobj.dump)
+ continue;
+
+ if (g_verbose)
+ write_msg(NULL, "reading row-security policy for table \"%s\"\n",
+ tbinfo->dobj.name);
+
+ /*
+ * select table schema to ensure regproc name is qualified if needed
+ */
+ selectSourceSchema(fout, tbinfo->dobj.namespace->dobj.name);
+
+ resetPQExpBuffer(query);
+
+ appendPQExpBuffer(query,
+ "SELECT oid, tableoid, s.rseccmd, "
+ "pg_get_expr(s.rsecqual, s.rsecrelid) AS rsecqual "
+ "FROM pg_catalog.pg_rowsecurity s "
+ "WHERE rsecrelid = '%u'",
+ tbinfo->dobj.catId.oid);
+ res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+
+ ntups = PQntuples(res);
+
+ i_oid = PQfnumber(res, "oid");
+ i_tableoid = PQfnumber(res, "tableoid");
+ i_rseccmd = PQfnumber(res, "rseccmd");
+ i_rsecqual = PQfnumber(res, "rsecqual");
+
+ rsinfo = pg_malloc(ntups * sizeof(RowSecurityInfo));
+ for (j=0; j < ntups; j++)
+ {
+ char namebuf[NAMEDATALEN + 1];
+
+ rsinfo[j].dobj.objType = DO_ROW_SECURITY;
+ rsinfo[j].dobj.catId.tableoid =
+ atooid(PQgetvalue(res, j, i_tableoid));
+ rsinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
+ AssignDumpId(&rsinfo[j].dobj);
+ snprintf(namebuf, sizeof(namebuf), "row-security of %s",
+ tbinfo->rolname);
+ rsinfo[j].dobj.name = namebuf;
+ rsinfo[j].dobj.namespace = tbinfo->dobj.namespace;
+ rsinfo[j].rstable = tbinfo;
+ rsinfo[j].rseccmd = pg_strdup(PQgetvalue(res, j, i_rseccmd));
+ rsinfo[j].rsecqual = pg_strdup(PQgetvalue(res, j, i_rsecqual));
+ }
+ PQclear(res);
+ }
+ destroyPQExpBuffer(query);
+}
+
+/*
+ * dumpRowSecurity
+ * dump the definition of the given row-security policy
+ */
+static void
+dumpRowSecurity(Archive *fout, RowSecurityInfo *rsinfo)
+{
+ TableInfo *tbinfo = rsinfo->rstable;
+ PQExpBuffer query;
+ PQExpBuffer delqry;
+ const char *cmd;
+
+ if (dataOnly)
+ return;
+
+ query = createPQExpBuffer();
+ delqry = createPQExpBuffer();
+ appendPQExpBuffer(query, "ALTER TABLE %s SET ROW SECURITY ",
+ fmtId(tbinfo->dobj.name));
+ appendPQExpBuffer(delqry, "ALTER TABLE %s RESET ROW SECURITY ",
+ fmtId(tbinfo->dobj.name));
+ if (strcmp(rsinfo->rseccmd, "a") == 0)
+ cmd = "ALL";
+ else if (strcmp(rsinfo->rseccmd, "s") == 0)
+ cmd = "SELECT";
+ else if (strcmp(rsinfo->rseccmd, "i") == 0)
+ cmd = "INSERT";
+ else if (strcmp(rsinfo->rseccmd, "u") == 0)
+ cmd = "UPDATE";
+ else if (strcmp(rsinfo->rseccmd, "d") == 0)
+ cmd = "DELETE";
+ else
+ {
+ write_msg(NULL, "unexpected command type: '%s'\n", rsinfo->rseccmd);
+ exit_nicely(1);
+ }
+ appendPQExpBuffer(query, "FOR %s TO (%s)", cmd, rsinfo->rsecqual);
+ appendPQExpBuffer(delqry, "FOR %s", cmd);
+
+ ArchiveEntry(fout, rsinfo->dobj.catId, rsinfo->dobj.dumpId,
+ rsinfo->dobj.name,
+ rsinfo->dobj.namespace->dobj.name,
+ NULL,
+ tbinfo->rolname, false,
+ "ROW SECURITY", SECTION_POST_DATA,
+ query->data, delqry->data, NULL,
+ NULL, 0,
+ NULL, NULL);
+
+ destroyPQExpBuffer(query);
+}
+
static void
binary_upgrade_set_type_oids_by_type_oid(Archive *fout,
PQExpBuffer upgrade_buffer,
@@ -4214,6 +4339,7 @@ getTables(Archive *fout, int *numTables)
int i_relhastriggers;
int i_relhasindex;
int i_relhasrules;
+ int i_relhasrowsec;
int i_relhasoids;
int i_relfrozenxid;
int i_toastoid;
@@ -4263,6 +4389,44 @@ getTables(Archive *fout, int *numTables)
"(%s c.relowner) AS rolname, "
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
+ "c.relhasrowsecurity, "
+ "c.relfrozenxid, tc.oid AS toid, "
+ "tc.relfrozenxid AS tfrozenxid, "
+ "c.relpersistence, "
+ "CASE WHEN c.reloftype <> 0 THEN c.reloftype::pg_catalog.regtype ELSE NULL END AS reloftype, "
+ "d.refobjid AS owning_tab, "
+ "d.refobjsubid AS owning_col, "
+ "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
+ "array_to_string(c.reloptions, ', ') AS reloptions, "
+ "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions "
+ "FROM pg_class c "
+ "LEFT JOIN pg_depend d ON "
+ "(c.relkind = '%c' AND "
+ "d.classid = c.tableoid AND d.objid = c.oid AND "
+ "d.objsubid = 0 AND "
+ "d.refclassid = c.tableoid AND d.deptype = 'a') "
+ "LEFT JOIN pg_class tc ON (c.reltoastrelid = tc.oid) "
+ "WHERE c.relkind in ('%c', '%c', '%c', '%c', '%c') "
+ "ORDER BY c.oid",
+ username_subquery,
+ RELKIND_SEQUENCE,
+ RELKIND_RELATION, RELKIND_SEQUENCE,
+ RELKIND_VIEW, RELKIND_COMPOSITE_TYPE,
+ RELKIND_FOREIGN_TABLE);
+ }
+ else if (fout->remoteVersion >= 90100)
+ {
+ /*
+ * Left join to pick up dependency info linking sequences to their
+ * owning column, if any (note this dependency is AUTO as of 8.2)
+ */
+ appendPQExpBuffer(query,
+ "SELECT c.tableoid, c.oid, c.relname, "
+ "c.relacl, c.relkind, c.relnamespace, "
+ "(%s c.relowner) AS rolname, "
+ "c.relchecks, c.relhastriggers, "
+ "c.relhasindex, c.relhasrules, c.relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"c.relfrozenxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"c.relpersistence, c.relispopulated, "
@@ -4337,6 +4501,7 @@ getTables(Archive *fout, int *numTables)
"(%s c.relowner) AS rolname, "
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"c.relfrozenxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"'p' AS relpersistence, 't' as relispopulated, "
@@ -4373,6 +4538,7 @@ getTables(Archive *fout, int *numTables)
"(%s c.relowner) AS rolname, "
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"c.relfrozenxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"'p' AS relpersistence, 't' as relispopulated, "
@@ -4409,6 +4575,7 @@ getTables(Archive *fout, int *numTables)
"(%s c.relowner) AS rolname, "
"c.relchecks, (c.reltriggers <> 0) AS relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"c.relfrozenxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"'p' AS relpersistence, 't' as relispopulated, "
@@ -4445,6 +4612,7 @@ getTables(Archive *fout, int *numTables)
"(%s relowner) AS rolname, "
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"0 AS relfrozenxid, "
"0 AS toid, "
"0 AS tfrozenxid, "
@@ -4481,6 +4649,7 @@ getTables(Archive *fout, int *numTables)
"(%s relowner) AS rolname, "
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"0 AS relfrozenxid, "
"0 AS toid, "
"0 AS tfrozenxid, "
@@ -4513,6 +4682,7 @@ getTables(Archive *fout, int *numTables)
"(%s relowner) AS rolname, "
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"0 AS relfrozenxid, "
"0 AS toid, "
"0 AS tfrozenxid, "
@@ -4540,6 +4710,7 @@ getTables(Archive *fout, int *numTables)
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, "
"'t'::bool AS relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"0 AS relfrozenxid, "
"0 AS toid, "
"0 AS tfrozenxid, "
@@ -4577,6 +4748,7 @@ getTables(Archive *fout, int *numTables)
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, "
"'t'::bool AS relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"0 as relfrozenxid, "
"0 AS toid, "
"0 AS tfrozenxid, "
@@ -4624,6 +4796,7 @@ getTables(Archive *fout, int *numTables)
i_relhastriggers = PQfnumber(res, "relhastriggers");
i_relhasindex = PQfnumber(res, "relhasindex");
i_relhasrules = PQfnumber(res, "relhasrules");
+ i_relhasrowsec = PQfnumber(res, "relhasrowsecurity");
i_relhasoids = PQfnumber(res, "relhasoids");
i_relfrozenxid = PQfnumber(res, "relfrozenxid");
i_toastoid = PQfnumber(res, "toid");
@@ -4671,6 +4844,7 @@ getTables(Archive *fout, int *numTables)
tblinfo[i].hasindex = (strcmp(PQgetvalue(res, i, i_relhasindex), "t") == 0);
tblinfo[i].hasrules = (strcmp(PQgetvalue(res, i, i_relhasrules), "t") == 0);
tblinfo[i].hastriggers = (strcmp(PQgetvalue(res, i, i_relhastriggers), "t") == 0);
+ tblinfo[i].hasrowsec = (strcmp(PQgetvalue(res, i, i_relhasrowsec), "t") == 0);
tblinfo[i].hasoids = (strcmp(PQgetvalue(res, i, i_relhasoids), "t") == 0);
tblinfo[i].relispopulated = (strcmp(PQgetvalue(res, i, i_relispopulated), "t") == 0);
tblinfo[i].relpages = atoi(PQgetvalue(res, i, i_relpages));
@@ -7748,6 +7922,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
NULL, 0,
dumpBlobs, NULL);
break;
+ case DO_ROW_SECURITY:
+ dumpRowSecurity(fout, (RowSecurityInfo *) dobj);
+ break;
case DO_PRE_DATA_BOUNDARY:
case DO_POST_DATA_BOUNDARY:
/* never dumped, nothing to do */
@@ -14910,6 +15087,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
case DO_TRIGGER:
case DO_EVENT_TRIGGER:
case DO_DEFAULT_ACL:
+ case DO_ROW_SECURITY:
/* Post-data objects: must come after the post-data boundary */
addObjectDependency(dobj, postDataBound->dumpId);
break;
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 5582538..31e009a 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -111,7 +111,8 @@ typedef enum
DO_PRE_DATA_BOUNDARY,
DO_POST_DATA_BOUNDARY,
DO_EVENT_TRIGGER,
- DO_REFRESH_MATVIEW
+ DO_REFRESH_MATVIEW,
+ DO_ROW_SECURITY,
} DumpableObjectType;
typedef struct _dumpableObject
@@ -243,6 +244,7 @@ typedef struct _tableInfo
bool hasindex; /* does it have any indexes? */
bool hasrules; /* does it have any rules? */
bool hastriggers; /* does it have any triggers? */
+ bool hasrowsec; /* does it have any row-security policy? */
bool hasoids; /* does it have OIDs? */
uint32 frozenxid; /* for restore frozen xid */
Oid toast_oid; /* for restore toast frozen xid */
@@ -481,6 +483,14 @@ typedef struct _blobInfo
char *blobacl;
} BlobInfo;
+typedef struct _rowSecurityInfo
+{
+ DumpableObject dobj;
+ TableInfo *rstable;
+ char *rseccmd;
+ char *rsecqual;
+} RowSecurityInfo;
+
/* global decls */
extern bool force_quotes; /* double-quotes for identifiers flag */
extern bool g_verbose; /* verbose flag */
@@ -572,5 +582,6 @@ extern DefaultACLInfo *getDefaultACLs(Archive *fout, int *numDefaultACLs);
extern void getExtensionMembership(Archive *fout, ExtensionInfo extinfo[],
int numExtensions);
extern EventTriggerInfo *getEventTriggers(Archive *fout, int *numEventTriggers);
+extern void getRowSecurity(Archive *fout, TableInfo tblinfo[], int numTables);
#endif /* PG_DUMP_H */
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 141e713..5c5777c 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -1342,6 +1342,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
"BLOB DATA (ID %d)",
obj->dumpId);
return;
+ case DO_ROW_SECURITY:
+ snprintf(buf, bufsize,
+ "ROW-SECURITY POLICY (ID %d OID %u)",
+ obj->dumpId, obj->catId.oid);
+ return;
case DO_PRE_DATA_BOUNDARY:
snprintf(buf, bufsize,
"PRE-DATA BOUNDARY (ID %d)",
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 9b6b9c2..db5c0e5 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2698,6 +2698,10 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys
appendPQExpBuffer(&buf,
",\n pg_catalog.obj_description(c.oid, 'pg_class') as \"%s\"",
gettext_noop("Description"));
+ if (pset.sversion >= 90300)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_get_expr(rs.rsecqual, c.oid) as \"%s\"",
+ gettext_noop("Row-security"));
}
appendPQExpBuffer(&buf,
@@ -2707,6 +2711,9 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys
appendPQExpBuffer(&buf,
"\n LEFT JOIN pg_catalog.pg_index i ON i.indexrelid = c.oid"
"\n LEFT JOIN pg_catalog.pg_class c2 ON i.indrelid = c2.oid");
+ if (verbose && pset.sversion >= 90300)
+ appendPQExpBuffer(&buf,
+ "\n LEFT JOIN pg_rowsecurity rs ON rs.rsecrelid = c.oid");
appendPQExpBuffer(&buf, "\nWHERE c.relkind IN (");
if (showTables)
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index 3aefbb5e..c96e8d4 100644
--- a/src/include/catalog/dependency.h
+++ b/src/include/catalog/dependency.h
@@ -147,6 +147,7 @@ typedef enum ObjectClass
OCLASS_DEFACL, /* pg_default_acl */
OCLASS_EXTENSION, /* pg_extension */
OCLASS_EVENT_TRIGGER, /* pg_event_trigger */
+ OCLASS_ROWSECURITY, /* pg_rowsecurity */
MAX_OCLASS /* MUST BE LAST */
} ObjectClass;
diff --git a/src/include/catalog/indexing.h b/src/include/catalog/indexing.h
index 19268fb..331a64b 100644
--- a/src/include/catalog/indexing.h
+++ b/src/include/catalog/indexing.h
@@ -311,6 +311,11 @@ DECLARE_UNIQUE_INDEX(pg_extension_name_index, 3081, on pg_extension using btree(
DECLARE_UNIQUE_INDEX(pg_range_rngtypid_index, 3542, on pg_range using btree(rngtypid oid_ops));
#define RangeTypidIndexId 3542
+DECLARE_UNIQUE_INDEX(pg_rowsecurity_oid_index, 3819, on pg_rowsecurity using btree(oid oid_ops));
+#define RowSecurityOidIndexId 3819
+DECLARE_UNIQUE_INDEX(pg_rowsecurity_relid_index, 3839, on pg_rowsecurity using btree(rsecrelid oid_ops, rseccmd char_ops));
+#define RowSecurityRelidIndexId 3839
+
/* last step of initialization script: build the indexes declared above */
BUILD_INDICES
diff --git a/src/include/catalog/pg_class.h b/src/include/catalog/pg_class.h
index 2225787..83d8e37 100644
--- a/src/include/catalog/pg_class.h
+++ b/src/include/catalog/pg_class.h
@@ -65,6 +65,7 @@ CATALOG(pg_class,1259) BKI_BOOTSTRAP BKI_ROWTYPE_OID(83) BKI_SCHEMA_MACRO
bool relhaspkey; /* has (or has had) PRIMARY KEY index */
bool relhasrules; /* has (or has had) any rules */
bool relhastriggers; /* has (or has had) any TRIGGERs */
+ bool relhasrowsecurity; /* has (or has had) row-security policy */
bool relhassubclass; /* has (or has had) derived classes */
bool relispopulated; /* matview currently holds query results */
TransactionId relfrozenxid; /* all Xids < this are frozen in this rel */
@@ -94,7 +95,7 @@ typedef FormData_pg_class *Form_pg_class;
* ----------------
*/
-#define Natts_pg_class 29
+#define Natts_pg_class 30
#define Anum_pg_class_relname 1
#define Anum_pg_class_relnamespace 2
#define Anum_pg_class_reltype 3
@@ -118,12 +119,13 @@ typedef FormData_pg_class *Form_pg_class;
#define Anum_pg_class_relhaspkey 21
#define Anum_pg_class_relhasrules 22
#define Anum_pg_class_relhastriggers 23
-#define Anum_pg_class_relhassubclass 24
-#define Anum_pg_class_relispopulated 25
-#define Anum_pg_class_relfrozenxid 26
-#define Anum_pg_class_relminmxid 27
-#define Anum_pg_class_relacl 28
-#define Anum_pg_class_reloptions 29
+#define Anum_pg_class_relhasrowsecurity 24
+#define Anum_pg_class_relhassubclass 25
+#define Anum_pg_class_relispopulated 26
+#define Anum_pg_class_relfrozenxid 27
+#define Anum_pg_class_relminmxid 28
+#define Anum_pg_class_relacl 29
+#define Anum_pg_class_reloptions 30
/* ----------------
* initial contents of pg_class
@@ -138,13 +140,13 @@ typedef FormData_pg_class *Form_pg_class;
* Note: "3" in the relfrozenxid column stands for FirstNormalTransactionId;
* similarly, "1" in relminmxid stands for FirstMultiXactId
*/
-DATA(insert OID = 1247 ( pg_type PGNSP 71 0 PGUID 0 0 0 0 0 0 0 0 f f p r 30 0 t f f f f t 3 1 _null_ _null_ ));
+DATA(insert OID = 1247 ( pg_type PGNSP 71 0 PGUID 0 0 0 0 0 0 0 0 f f p r 30 0 t f f f f f t 3 1 _null_ _null_ ));
DESCR("");
-DATA(insert OID = 1249 ( pg_attribute PGNSP 75 0 PGUID 0 0 0 0 0 0 0 0 f f p r 21 0 f f f f f t 3 1 _null_ _null_ ));
+DATA(insert OID = 1249 ( pg_attribute PGNSP 75 0 PGUID 0 0 0 0 0 0 0 0 f f p r 21 0 f f f f f f t 3 1 _null_ _null_ ));
DESCR("");
-DATA(insert OID = 1255 ( pg_proc PGNSP 81 0 PGUID 0 0 0 0 0 0 0 0 f f p r 27 0 t f f f f t 3 1 _null_ _null_ ));
+DATA(insert OID = 1255 ( pg_proc PGNSP 81 0 PGUID 0 0 0 0 0 0 0 0 f f p r 27 0 t f f f f f t 3 1 _null_ _null_ ));
DESCR("");
-DATA(insert OID = 1259 ( pg_class PGNSP 83 0 PGUID 0 0 0 0 0 0 0 0 f f p r 29 0 t f f f f t 3 1 _null_ _null_ ));
+DATA(insert OID = 1259 ( pg_class PGNSP 83 0 PGUID 0 0 0 0 0 0 0 0 f f p r 30 0 t f f f f f t 3 1 _null_ _null_ ));
DESCR("");
diff --git a/src/include/catalog/pg_rowsecurity.h b/src/include/catalog/pg_rowsecurity.h
new file mode 100644
index 0000000..a068b35
--- /dev/null
+++ b/src/include/catalog/pg_rowsecurity.h
@@ -0,0 +1,76 @@
+/*
+ * pg_rowsecurity.h
+ * definition of the system catalog for row-security policy (pg_rowsecurity)
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ */
+#ifndef PG_ROWSECURITY_H
+#define PG_ROWSECURITY_H
+
+#include "catalog/genbki.h"
+#include "nodes/primnodes.h"
+#include "utils/memutils.h"
+#include "utils/relcache.h"
+
+/* ----------------
+ * pg_rowlevelsec definition. cpp turns this into
+ * typedef struct FormData_pg_rowlevelsec
+ * ----------------
+ */
+#define RowSecurityRelationId 3838
+
+CATALOG(pg_rowsecurity,3838)
+{
+ /* Oid of the relation that has row-security policy */
+ Oid rsecrelid;
+
+ /* One of ROWSECURITY_CMD_* below */
+ char rseccmd;
+#ifdef CATALOG_VARLEN
+ pg_node_tree rsecqual;
+#endif
+} FormData_pg_rowsecurity;
+
+/* ----------------
+ * Form_pg_rowlevelsec corresponds to a pointer to a row with
+ * the format of pg_rowlevelsec relation.
+ * ----------------
+ */
+typedef FormData_pg_rowsecurity *Form_pg_rowsecurity;
+
+/* ----------------
+ * compiler constants for pg_rowlevelsec
+ * ----------------
+ */
+#define Natts_pg_rowsecurity 3
+#define Anum_pg_rowsecurity_rsecrelid 1
+#define Anum_pg_rowsecurity_rseccmd 2
+#define Anum_pg_rowsecurity_rsecqual 3
+
+#define ROWSECURITY_CMD_ALL 'a'
+#define ROWSECURITY_CMD_SELECT 's'
+#define ROWSECURITY_CMD_INSERT 'i'
+#define ROWSECURITY_CMD_UPDATE 'u'
+#define ROWSECURITY_CMD_DELETE 'd'
+
+typedef struct
+{
+ Oid rsecid;
+ Expr *qual;
+ bool hassublinks;
+} RowSecurityEntry;
+
+typedef struct
+{
+ MemoryContext rscxt;
+ RowSecurityEntry rsall; /* row-security policy for ALL */
+} RowSecurityDesc;
+
+extern void RelationBuildRowSecurity(Relation relation);
+extern void ATExecSetRowSecurity(Relation relation,
+ const char *cmdname, Node *clause);
+extern void RemoveRowSecurityById(Oid relationId);
+
+#endif /* PG_ROWSECURITY_H */
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index be3add9..d266aa9 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -278,6 +278,7 @@ extern int trace_recovery(int trace_level);
/* flags to be OR'd to form sec_context */
#define SECURITY_LOCAL_USERID_CHANGE 0x0001
#define SECURITY_RESTRICTED_OPERATION 0x0002
+#define SECURITY_ROW_LEVEL_DISABLED 0x0004
extern char *DatabasePath;
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 4f77016..f49cbf9 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -306,6 +306,8 @@ typedef struct JunkFilter
* ConstraintExprs array of constraint-checking expr states
* junkFilter for removing junk attributes from tuples
* projectReturning for computing a RETURNING list
+ * rowSecurity for row-security checks
+ * rowSecParams param-list if row-security has SubLink
* ----------------
*/
typedef struct ResultRelInfo
@@ -325,6 +327,8 @@ typedef struct ResultRelInfo
List **ri_ConstraintExprs;
JunkFilter *ri_junkFilter;
ProjectionInfo *ri_projectReturning;
+ Node *ri_rowSecurity;
+ List *ri_rowSecParams;
} ResultRelInfo;
/* ----------------
diff --git a/src/include/nodes/nodeFuncs.h b/src/include/nodes/nodeFuncs.h
index d4901ca..cb30110 100644
--- a/src/include/nodes/nodeFuncs.h
+++ b/src/include/nodes/nodeFuncs.h
@@ -24,6 +24,7 @@
#define QTW_IGNORE_RANGE_TABLE 0x08 /* skip rangetable entirely */
#define QTW_EXAMINE_RTES 0x10 /* examine RTEs */
#define QTW_DONT_COPY_QUERY 0x20 /* do not copy top Query */
+#define QTW_IGNORE_RETURNING 0x40 /* skip returning clause */
extern Oid exprType(const Node *expr);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 6723647..fbf4c80 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -31,7 +31,8 @@ typedef enum QuerySource
QSRC_PARSER, /* added by parse analysis (now unused) */
QSRC_INSTEAD_RULE, /* added by unconditional INSTEAD rule */
QSRC_QUAL_INSTEAD_RULE, /* added by conditional INSTEAD rule */
- QSRC_NON_INSTEAD_RULE /* added by non-INSTEAD rule */
+ QSRC_NON_INSTEAD_RULE, /* added by non-INSTEAD rule */
+ QSRC_ROW_SECURITY, /* added by row-security */
} QuerySource;
/* Sort ordering options for ORDER BY and CREATE INDEX */
@@ -112,7 +113,9 @@ typedef struct Query
int resultRelation; /* rtable index of target relation for
* INSERT/UPDATE/DELETE; 0 for SELECT */
-
+ int sourceRelation; /* rtable index of source relation for
+ * UPDATE/DELETE, if not identical with
+ * resultRelation; 0 for elsewhere */
bool hasAggs; /* has aggregates in tlist or havingQual */
bool hasWindowFuncs; /* has window functions in tlist */
bool hasSubLinks; /* has subquery SubLink */
@@ -719,6 +722,11 @@ typedef struct RangeTblEntry
*/
Query *subquery; /* the sub-query */
bool security_barrier; /* is from security_barrier view? */
+ Oid rowsec_relid; /* OID of the original relation, if this
+ * sub-query originated from row-security
+ * policy on the relation. Elsewhere, it
+ * should be InvalidOid.
+ */
/*
* Fields valid for a join RTE (else NULL/zero):
@@ -1244,6 +1252,8 @@ typedef enum AlterTableType
AT_DropInherit, /* NO INHERIT parent */
AT_AddOf, /* OF <type_name> */
AT_DropOf, /* NOT OF */
+ AT_SetRowSecurity, /* SET ROW SECURITY (...) */
+ AT_ResetRowSecurity, /* RESET ROW SECURITY */
AT_GenericOptions /* OPTIONS (...) */
} AlterTableType;
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 841701e..cc94421 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -67,6 +67,8 @@ typedef struct PlannedStmt
List *invalItems; /* other dependencies, as PlanInvalItems */
int nParamExec; /* number of PARAM_EXEC Params used */
+
+ Oid planUserId; /* user-id this plan assumed, or InvalidOid */
} PlannedStmt;
/* macro for fetching the Plan associated with a SubPlan node */
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index c0a636b..ed01b65 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -98,6 +98,8 @@ typedef struct PlannerGlobal
Index lastRowMarkId; /* highest PlanRowMark ID assigned */
bool transientPlan; /* redo plan when TransactionXmin changes? */
+
+ Oid planUserId; /* User-Id to be assumed on this plan */
} PlannerGlobal;
/* macro for fetching the Plan associated with a SubPlan node */
@@ -1416,6 +1418,10 @@ typedef struct AppendRelInfo
*/
Index parent_relid; /* RT index of append parent rel */
Index child_relid; /* RT index of append child rel */
+ Index child_result; /* RT index of append child rel's source,
+ * if source of result relation is not
+ * identical. Elsewhere, 0.
+ */
/*
* For an inheritance appendrel, the parent and child are both regular
diff --git a/src/include/optimizer/rowsecurity.h b/src/include/optimizer/rowsecurity.h
new file mode 100644
index 0000000..ff4dd9a
--- /dev/null
+++ b/src/include/optimizer/rowsecurity.h
@@ -0,0 +1,27 @@
+/* -------------------------------------------------------------------------
+ *
+ * rowsecurity.h
+ * prototypes for optimizer/rowsecurity.c
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * -------------------------------------------------------------------------
+ */
+#ifndef ROWSECURITY_H
+#define ROWSECURITY_H
+
+#include "nodes/execnodes.h"
+#include "nodes/parsenodes.h"
+#include "nodes/relation.h"
+#include "utils/rel.h"
+
+typedef List *(*row_security_policy_hook_type)(CmdType cmdtype,
+ Relation relation);
+extern PGDLLIMPORT row_security_policy_hook_type row_security_policy_hook;
+
+extern bool copy_row_security_policy(CopyStmt *stmt,
+ Relation relation, List *attnums);
+extern void apply_row_security_policy(PlannerInfo *root);
+
+#endif /* ROWSECURITY_H */
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index 49ca764..a3d5be1 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -62,7 +62,8 @@ typedef enum ParseExprKind
EXPR_KIND_INDEX_PREDICATE, /* index predicate */
EXPR_KIND_ALTER_COL_TRANSFORM, /* transform expr in ALTER COLUMN TYPE */
EXPR_KIND_EXECUTE_PARAMETER, /* parameter value in EXECUTE */
- EXPR_KIND_TRIGGER_WHEN /* WHEN condition in CREATE TRIGGER */
+ EXPR_KIND_TRIGGER_WHEN, /* WHEN condition in CREATE TRIGGER */
+ EXPR_KIND_ROW_SECURITY, /* ROW SECURITY policy for a table */
} ParseExprKind;
diff --git a/src/include/parser/parsetree.h b/src/include/parser/parsetree.h
index 9608cc6..b4e9df4 100644
--- a/src/include/parser/parsetree.h
+++ b/src/include/parser/parsetree.h
@@ -32,14 +32,11 @@
((RangeTblEntry *) list_nth(rangetable, (rangetable_index)-1))
/*
- * getrelid
- *
* Given the range index of a relation, return the corresponding
* relation OID. Note that InvalidOid will be returned if the
* RTE is for a non-relation-type RTE.
*/
-#define getrelid(rangeindex,rangetable) \
- (rt_fetch(rangeindex, rangetable)->relid)
+extern Oid getrelid(Index rangeindex, List *rangetable);
/*
* Given an RTE and an attribute number, return the appropriate
diff --git a/src/include/rewrite/rewriteHandler.h b/src/include/rewrite/rewriteHandler.h
index 5983315..840807a 100644
--- a/src/include/rewrite/rewriteHandler.h
+++ b/src/include/rewrite/rewriteHandler.h
@@ -18,6 +18,7 @@
#include "nodes/parsenodes.h"
extern List *QueryRewrite(Query *parsetree);
+extern void QueryRewriteExpr(Node *node, List *activeRIRs);
extern void AcquireRewriteLocks(Query *parsetree, bool forUpdatePushedDown);
extern Node *build_column_default(Relation rel, int attrno);
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index 72f8491..35819fa 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -128,6 +128,8 @@ typedef struct CachedPlan
bool is_oneshot; /* is it a "oneshot" plan? */
bool is_saved; /* is CachedPlan in a long-lived context? */
bool is_valid; /* is the stmt_list currently valid? */
+ Oid planUserId; /* is user-id that is assumed on this cached
+ plan, or InvalidOid if portable for anybody */
TransactionId saved_xmin; /* if valid, replan when TransactionXmin
* changes from this value */
int generation; /* parent's generation number for this plan */
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 58cc3f7..5e8441c 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -18,6 +18,7 @@
#include "catalog/pg_am.h"
#include "catalog/pg_class.h"
#include "catalog/pg_index.h"
+#include "catalog/pg_rowsecurity.h"
#include "fmgr.h"
#include "nodes/bitmapset.h"
#include "rewrite/prs2lock.h"
@@ -109,6 +110,7 @@ typedef struct RelationData
RuleLock *rd_rules; /* rewrite rules */
MemoryContext rd_rulescxt; /* private memory cxt for rd_rules, if any */
TriggerDesc *trigdesc; /* Trigger info, or NULL if rel has none */
+ RowSecurityDesc *rsdesc; /* Row-security policy, or NULL */
/*
* rd_options is set whenever rd_rel is loaded into the relcache entry.
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
new file mode 100644
index 0000000..04591df
--- /dev/null
+++ b/src/test/regress/expected/rowsecurity.out
@@ -0,0 +1,950 @@
+--
+-- Test of Row-level security feature
+--
+-- Clean up in case a prior regression run failed
+-- Suppress NOTICE messages when users/groups don't exist
+SET client_min_messages TO 'warning';
+DROP USER IF EXISTS rls_regress_user0;
+DROP USER IF EXISTS rls_regress_user1;
+DROP USER IF EXISTS rls_regress_user2;
+DROP SCHEMA IF EXISTS rls_regress_schema CASCADE;
+RESET client_min_messages;
+-- initial setup
+CREATE USER rls_regress_user0;
+CREATE USER rls_regress_user1;
+CREATE USER rls_regress_user2;
+CREATE SCHEMA rls_regress_schema;
+GRANT ALL ON SCHEMA rls_regress_schema TO public;
+SET search_path = rls_regress_schema;
+-- setup of malicious function
+CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool
+ COST 0.0000001 LANGUAGE plpgsql
+ AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END';
+GRANT EXECUTE ON FUNCTION f_leak(text) TO public;
+-- BASIC Row-Level Security Scenario
+SET SESSION AUTHORIZATION rls_regress_user0;
+CREATE TABLE uaccount (
+ pguser name primary key,
+ seclv int
+);
+INSERT INTO uaccount VALUES
+ ('rls_regress_user0', 99),
+ ('rls_regress_user1', 1),
+ ('rls_regress_user2', 2),
+ ('rls_regress_user3', 3);
+GRANT SELECT ON uaccount TO public;
+CREATE TABLE category (
+ cid int primary key,
+ cname text
+);
+GRANT ALL ON category TO public;
+INSERT INTO category VALUES
+ (11, 'novel'),
+ (22, 'science fiction'),
+ (33, 'technology'),
+ (44, 'manga');
+CREATE TABLE document (
+ did int primary key,
+ cid int references category(cid),
+ dlevel int not null,
+ dauthor name,
+ dtitle text
+);
+GRANT ALL ON document TO public;
+INSERT INTO document VALUES
+ ( 1, 11, 1, 'rls_regress_user1', 'my first novel'),
+ ( 2, 11, 2, 'rls_regress_user1', 'my second novel'),
+ ( 3, 22, 2, 'rls_regress_user1', 'my science fiction'),
+ ( 4, 44, 1, 'rls_regress_user1', 'my first manga'),
+ ( 5, 44, 2, 'rls_regress_user1', 'my second manga'),
+ ( 6, 22, 1, 'rls_regress_user2', 'great science fiction'),
+ ( 7, 33, 2, 'rls_regress_user2', 'great technology book'),
+ ( 8, 44, 1, 'rls_regress_user2', 'great manga');
+-- user's security level must higher than or equal to document's one
+ALTER TABLE document SET ROW SECURITY FOR ALL
+ TO (dlevel <= (SELECT seclv FROM uaccount WHERE pguser = current_user));
+-- viewpoint from rls_regress_user1
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE: f_leak => my first novel
+NOTICE: f_leak => my first manga
+NOTICE: f_leak => great science fiction
+NOTICE: f_leak => great manga
+ did | cid | dlevel | dauthor | dtitle
+-----+-----+--------+-------------------+-----------------------
+ 1 | 11 | 1 | rls_regress_user1 | my first novel
+ 4 | 44 | 1 | rls_regress_user1 | my first manga
+ 6 | 22 | 1 | rls_regress_user2 | great science fiction
+ 8 | 44 | 1 | rls_regress_user2 | great manga
+(4 rows)
+
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+NOTICE: f_leak => my first novel
+NOTICE: f_leak => my first manga
+NOTICE: f_leak => great science fiction
+NOTICE: f_leak => great manga
+ cid | did | dlevel | dauthor | dtitle | cname
+-----+-----+--------+-------------------+-----------------------+-----------------
+ 11 | 1 | 1 | rls_regress_user1 | my first novel | novel
+ 22 | 6 | 1 | rls_regress_user2 | great science fiction | science fiction
+ 44 | 8 | 1 | rls_regress_user2 | great manga | manga
+ 44 | 4 | 1 | rls_regress_user1 | my first manga | manga
+(4 rows)
+
+-- viewpoint from rls_regress_user2
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE: f_leak => my first novel
+NOTICE: f_leak => my second novel
+NOTICE: f_leak => my science fiction
+NOTICE: f_leak => my first manga
+NOTICE: f_leak => my second manga
+NOTICE: f_leak => great science fiction
+NOTICE: f_leak => great technology book
+NOTICE: f_leak => great manga
+ did | cid | dlevel | dauthor | dtitle
+-----+-----+--------+-------------------+-----------------------
+ 1 | 11 | 1 | rls_regress_user1 | my first novel
+ 2 | 11 | 2 | rls_regress_user1 | my second novel
+ 3 | 22 | 2 | rls_regress_user1 | my science fiction
+ 4 | 44 | 1 | rls_regress_user1 | my first manga
+ 5 | 44 | 2 | rls_regress_user1 | my second manga
+ 6 | 22 | 1 | rls_regress_user2 | great science fiction
+ 7 | 33 | 2 | rls_regress_user2 | great technology book
+ 8 | 44 | 1 | rls_regress_user2 | great manga
+(8 rows)
+
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+NOTICE: f_leak => my first novel
+NOTICE: f_leak => my second novel
+NOTICE: f_leak => my science fiction
+NOTICE: f_leak => my first manga
+NOTICE: f_leak => my second manga
+NOTICE: f_leak => great science fiction
+NOTICE: f_leak => great technology book
+NOTICE: f_leak => great manga
+ cid | did | dlevel | dauthor | dtitle | cname
+-----+-----+--------+-------------------+-----------------------+-----------------
+ 11 | 2 | 2 | rls_regress_user1 | my second novel | novel
+ 11 | 1 | 1 | rls_regress_user1 | my first novel | novel
+ 22 | 6 | 1 | rls_regress_user2 | great science fiction | science fiction
+ 22 | 3 | 2 | rls_regress_user1 | my science fiction | science fiction
+ 33 | 7 | 2 | rls_regress_user2 | great technology book | technology
+ 44 | 8 | 1 | rls_regress_user2 | great manga | manga
+ 44 | 5 | 2 | rls_regress_user1 | my second manga | manga
+ 44 | 4 | 1 | rls_regress_user1 | my first manga | manga
+(8 rows)
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+ QUERY PLAN
+----------------------------------------------------------
+ Subquery Scan on document
+ Filter: f_leak(document.dtitle)
+ -> Seq Scan on document document_1
+ Filter: (dlevel <= $0)
+ InitPlan 1 (returns $0)
+ -> Index Scan using uaccount_pkey on uaccount
+ Index Cond: (pguser = "current_user"())
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+ QUERY PLAN
+----------------------------------------------------------------------
+ Hash Join
+ Hash Cond: (category.cid = document.cid)
+ -> Seq Scan on category
+ -> Hash
+ -> Subquery Scan on document
+ Filter: f_leak(document.dtitle)
+ -> Seq Scan on document document_1
+ Filter: (dlevel <= $0)
+ InitPlan 1 (returns $0)
+ -> Index Scan using uaccount_pkey on uaccount
+ Index Cond: (pguser = "current_user"())
+(11 rows)
+
+-- only owner can change row-level security
+ALTER TABLE document SET ROW SECURITY FOR ALL TO (true); -- fail
+ERROR: must be owner of relation document
+ALTER TABLE document RESET ROW SECURITY FOR ALL; -- fail
+ERROR: must be owner of relation document
+SET SESSION AUTHORIZATION rls_regress_user0;
+ALTER TABLE document SET ROW SECURITY FOR ALL TO (dauthor = current_user);
+-- viewpoint from rls_regress_user1 again
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE: f_leak => my first novel
+NOTICE: f_leak => my second novel
+NOTICE: f_leak => my science fiction
+NOTICE: f_leak => my first manga
+NOTICE: f_leak => my second manga
+ did | cid | dlevel | dauthor | dtitle
+-----+-----+--------+-------------------+--------------------
+ 1 | 11 | 1 | rls_regress_user1 | my first novel
+ 2 | 11 | 2 | rls_regress_user1 | my second novel
+ 3 | 22 | 2 | rls_regress_user1 | my science fiction
+ 4 | 44 | 1 | rls_regress_user1 | my first manga
+ 5 | 44 | 2 | rls_regress_user1 | my second manga
+(5 rows)
+
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+NOTICE: f_leak => my first novel
+NOTICE: f_leak => my second novel
+NOTICE: f_leak => my science fiction
+NOTICE: f_leak => my first manga
+NOTICE: f_leak => my second manga
+ cid | did | dlevel | dauthor | dtitle | cname
+-----+-----+--------+-------------------+--------------------+-----------------
+ 11 | 1 | 1 | rls_regress_user1 | my first novel | novel
+ 11 | 2 | 2 | rls_regress_user1 | my second novel | novel
+ 22 | 3 | 2 | rls_regress_user1 | my science fiction | science fiction
+ 44 | 4 | 1 | rls_regress_user1 | my first manga | manga
+ 44 | 5 | 2 | rls_regress_user1 | my second manga | manga
+(5 rows)
+
+-- viewpoint from rls_regress_user2 again
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE: f_leak => great science fiction
+NOTICE: f_leak => great technology book
+NOTICE: f_leak => great manga
+ did | cid | dlevel | dauthor | dtitle
+-----+-----+--------+-------------------+-----------------------
+ 6 | 22 | 1 | rls_regress_user2 | great science fiction
+ 7 | 33 | 2 | rls_regress_user2 | great technology book
+ 8 | 44 | 1 | rls_regress_user2 | great manga
+(3 rows)
+
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+NOTICE: f_leak => great science fiction
+NOTICE: f_leak => great technology book
+NOTICE: f_leak => great manga
+ cid | did | dlevel | dauthor | dtitle | cname
+-----+-----+--------+-------------------+-----------------------+-----------------
+ 22 | 6 | 1 | rls_regress_user2 | great science fiction | science fiction
+ 33 | 7 | 2 | rls_regress_user2 | great technology book | technology
+ 44 | 8 | 1 | rls_regress_user2 | great manga | manga
+(3 rows)
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+ QUERY PLAN
+----------------------------------------------
+ Subquery Scan on document
+ Filter: f_leak(document.dtitle)
+ -> Seq Scan on document document_1
+ Filter: (dauthor = "current_user"())
+(4 rows)
+
+EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+ QUERY PLAN
+----------------------------------------------------
+ Nested Loop
+ -> Subquery Scan on document
+ Filter: f_leak(document.dtitle)
+ -> Seq Scan on document document_1
+ Filter: (dauthor = "current_user"())
+ -> Index Scan using category_pkey on category
+ Index Cond: (cid = document.cid)
+(7 rows)
+
+-- interaction of FK/PK constraints
+SET SESSION AUTHORIZATION rls_regress_user0;
+ALTER TABLE category SET ROW SECURITY FOR ALL
+ TO (CASE WHEN current_user = 'rls_regress_user1' THEN cid IN (11, 33)
+ WHEN current_user = 'rls_regress_user2' THEN cid IN (22, 44)
+ ELSE false END);
+-- cannot delete PK referenced by invisible FK
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document d full outer join category c on d.cid = c.cid;
+ did | cid | dlevel | dauthor | dtitle | cid | cname
+-----+-----+--------+-------------------+--------------------+-----+------------
+ 2 | 11 | 2 | rls_regress_user1 | my second novel | 11 | novel
+ 1 | 11 | 1 | rls_regress_user1 | my first novel | 11 | novel
+ | | | | | 33 | technology
+ 5 | 44 | 2 | rls_regress_user1 | my second manga | |
+ 4 | 44 | 1 | rls_regress_user1 | my first manga | |
+ 3 | 22 | 2 | rls_regress_user1 | my science fiction | |
+(6 rows)
+
+DELETE FROM category WHERE cid = 33; -- failed
+ERROR: update or delete on table "category" violates foreign key constraint "document_cid_fkey" on table "document"
+DETAIL: Key (cid)=(33) is still referenced from table "document".
+-- cannot insert FK referencing invisible PK
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document d full outer join category c on d.cid = c.cid;
+ did | cid | dlevel | dauthor | dtitle | cid | cname
+-----+-----+--------+-------------------+-----------------------+-----+-----------------
+ 6 | 22 | 1 | rls_regress_user2 | great science fiction | 22 | science fiction
+ 8 | 44 | 1 | rls_regress_user2 | great manga | 44 | manga
+ 7 | 33 | 2 | rls_regress_user2 | great technology book | |
+(3 rows)
+
+INSERT INTO document VALUES (10, 33, 1, current_user, 'hoge'); -- failed
+ERROR: insert or update on table "document" violates foreign key constraint "document_cid_fkey"
+DETAIL: Key (cid)=(33) is not present in table "category".
+-- database superuser can bypass RLS policy
+RESET SESSION AUTHORIZATION;
+SELECT * FROM document;
+ did | cid | dlevel | dauthor | dtitle
+-----+-----+--------+-------------------+-----------------------
+ 1 | 11 | 1 | rls_regress_user1 | my first novel
+ 2 | 11 | 2 | rls_regress_user1 | my second novel
+ 3 | 22 | 2 | rls_regress_user1 | my science fiction
+ 4 | 44 | 1 | rls_regress_user1 | my first manga
+ 5 | 44 | 2 | rls_regress_user1 | my second manga
+ 6 | 22 | 1 | rls_regress_user2 | great science fiction
+ 7 | 33 | 2 | rls_regress_user2 | great technology book
+ 8 | 44 | 1 | rls_regress_user2 | great manga
+(8 rows)
+
+SELECT * FROM category;
+ cid | cname
+-----+-----------------
+ 11 | novel
+ 22 | science fiction
+ 33 | technology
+ 44 | manga
+(4 rows)
+
+--
+-- Table inheritance and RLS policy
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+CREATE TABLE t1 (a int, junk1 text, b text) WITH OIDS;
+ALTER TABLE t1 DROP COLUMN junk1; -- just a disturbing factor
+GRANT ALL ON t1 TO public;
+COPY t1 FROM stdin WITH (oids);
+CREATE TABLE t2 (c float) INHERITS (t1);
+COPY t2 FROM stdin WITH (oids);
+CREATE TABLE t3 (c text, b text, a int) WITH OIDS;
+ALTER TABLE t3 INHERIT t1;
+COPY t3(a,b,c) FROM stdin WITH (oids);
+ALTER TABLE t1 SET ROW SECURITY FOR ALL TO (a % 2 = 0); -- be even number
+ALTER TABLE t2 SET ROW SECURITY FOR ALL TO (a % 2 = 1); -- be odd number
+SELECT * FROM t1;
+ a | b
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1;
+ QUERY PLAN
+-------------------------------------
+ Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+(8 rows)
+
+SELECT * FROM t1 WHERE f_leak(b);
+NOTICE: f_leak => bbb
+NOTICE: f_leak => ddd
+NOTICE: f_leak => abc
+NOTICE: f_leak => cde
+NOTICE: f_leak => xxx
+NOTICE: f_leak => yyy
+NOTICE: f_leak => zzz
+ a | b
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+ QUERY PLAN
+-------------------------------------
+ Append
+ -> Subquery Scan on t1
+ Filter: f_leak(t1.b)
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ Filter: f_leak(t2.b)
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+ Filter: f_leak(b)
+(11 rows)
+
+-- reference to system column
+SELECT oid, * FROM t1;
+ oid | a | b
+-----+---+-----
+ 102 | 2 | bbb
+ 104 | 4 | ddd
+ 201 | 1 | abc
+ 203 | 3 | cde
+ 301 | 1 | xxx
+ 302 | 2 | yyy
+ 303 | 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1;
+ QUERY PLAN
+-------------------------------------
+ Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+(8 rows)
+
+-- reference to whole-row reference
+SELECT *,t1 FROM t1;
+ a | b | t1
+---+-----+---------
+ 2 | bbb | (2,bbb)
+ 4 | ddd | (4,ddd)
+ 1 | abc | (1,abc)
+ 3 | cde | (3,cde)
+ 1 | xxx | (1,xxx)
+ 2 | yyy | (2,yyy)
+ 3 | zzz | (3,zzz)
+(7 rows)
+
+EXPLAIN (costs off) SELECT *,t1 FROM t1;
+ QUERY PLAN
+-------------------------------------
+ Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+(8 rows)
+
+-- for share/update lock
+SELECT * FROM t1 FOR SHARE;
+ a | b
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 FOR SHARE;
+ QUERY PLAN
+-------------------------------------------
+ LockRows
+ -> Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+(9 rows)
+
+SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+NOTICE: f_leak => bbb
+NOTICE: f_leak => ddd
+NOTICE: f_leak => abc
+NOTICE: f_leak => cde
+NOTICE: f_leak => xxx
+NOTICE: f_leak => yyy
+NOTICE: f_leak => zzz
+ a | b
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+ QUERY PLAN
+-------------------------------------------
+ LockRows
+ -> Append
+ -> Subquery Scan on t1
+ Filter: f_leak(t1.b)
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ Filter: f_leak(t2.b)
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+ Filter: f_leak(b)
+(12 rows)
+
+--
+-- COPY TO statement
+--
+COPY t1 TO stdout;
+2 bbb
+4 ddd
+COPY t1 TO stdout WITH OIDS;
+102 2 bbb
+104 4 ddd
+COPY t2(c,b) TO stdout WITH OIDS;
+201 1.1 abc
+203 3.3 cde
+COPY (SELECT * FROM t1) TO stdout;
+2 bbb
+4 ddd
+1 abc
+3 cde
+1 xxx
+2 yyy
+3 zzz
+COPY document TO stdout WITH OIDS; -- failed (no oid column)
+ERROR: table "document" does not have OIDs
+--
+-- recursive RLS and VIEWs in policy
+--
+CREATE TABLE s1 (a int, b text);
+INSERT INTO s1 (SELECT x, md5(x::text) FROM generate_series(-10,10) x);
+CREATE TABLE s2 (x int, y text);
+INSERT INTO s2 (SELECT x, md5(x::text) FROM generate_series(-6,6) x);
+CREATE VIEW v2 AS SELECT * FROM s2 WHERE y like '%af%';
+ALTER TABLE s1 SET ROW SECURITY FOR ALL
+ TO (a in (select x from s2 where y like '%2f%'));
+ALTER TABLE s2 SET ROW SECURITY FOR ALL
+ TO (x in (select a from s1 where b like '%22%'));
+SELECT * FROM s1 WHERE f_leak(b); -- fail (infinite recursion)
+ERROR: infinite recursion detected for relation "s1"
+ALTER TABLE s2 SET ROW SECURITY FOR ALL TO (x % 2 = 0);
+SELECT * FROM s1 WHERE f_leak(b); -- OK
+NOTICE: f_leak => c81e728d9d4c2f636f067f89cc14862c
+NOTICE: f_leak => a87ff679a2f3e71d9181a67b7542122c
+ a | b
+---+----------------------------------
+ 2 | c81e728d9d4c2f636f067f89cc14862c
+ 4 | a87ff679a2f3e71d9181a67b7542122c
+(2 rows)
+
+EXPLAIN SELECT * FROM only s1 WHERE f_leak(b);
+ QUERY PLAN
+---------------------------------------------------------------------------------------
+ Subquery Scan on s1 (cost=28.55..61.67 rows=205 width=36)
+ Filter: f_leak(s1.b)
+ -> Hash Join (cost=28.55..55.52 rows=615 width=36)
+ Hash Cond: (s1_1.a = s2.x)
+ -> Seq Scan on s1 s1_1 (cost=0.00..22.30 rows=1230 width=36)
+ -> Hash (cost=28.54..28.54 rows=1 width=4)
+ -> HashAggregate (cost=28.53..28.54 rows=1 width=4)
+ -> Subquery Scan on s2 (cost=0.00..28.52 rows=1 width=4)
+ Filter: (s2.y ~~ '%2f%'::text)
+ -> Seq Scan on s2 s2_1 (cost=0.00..28.45 rows=6 width=36)
+ Filter: ((x % 2) = 0)
+(11 rows)
+
+ALTER TABLE s1 SET ROW SECURITY FOR ALL
+ TO (a in (select x from v2)); -- using VIEW in RLS policy
+SELECT * FROM s1 WHERE f_leak(b); -- OK
+NOTICE: f_leak => 0267aaf632e87a63288a08331f22c7c3
+NOTICE: f_leak => 1679091c5a880faf6fb5e6087eb1b2dc
+ a | b
+----+----------------------------------
+ -4 | 0267aaf632e87a63288a08331f22c7c3
+ 6 | 1679091c5a880faf6fb5e6087eb1b2dc
+(2 rows)
+
+EXPLAIN (COSTS OFF) SELECT * FROM s1 WHERE f_leak(b);
+ QUERY PLAN
+----------------------------------------------------------
+ Subquery Scan on s1
+ Filter: f_leak(s1.b)
+ -> Hash Join
+ Hash Cond: (s1_1.a = s2.x)
+ -> Seq Scan on s1 s1_1
+ -> Hash
+ -> HashAggregate
+ -> Subquery Scan on s2
+ Filter: (s2.y ~~ '%af%'::text)
+ -> Seq Scan on s2 s2_1
+ Filter: ((x % 2) = 0)
+(11 rows)
+
+SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+ xx | x | y
+----+----+----------------------------------
+ -6 | -6 | 596a3d04481816330f07e4f97510c28f
+ -4 | -4 | 0267aaf632e87a63288a08331f22c7c3
+ 2 | 2 | c81e728d9d4c2f636f067f89cc14862c
+(3 rows)
+
+EXPLAIN (COSTS OFF) SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+ QUERY PLAN
+--------------------------------------------------------------------
+ Subquery Scan on s2
+ Filter: (s2.y ~~ '%28%'::text)
+ -> Seq Scan on s2 s2_1
+ Filter: ((x % 2) = 0)
+ SubPlan 1
+ -> Limit
+ -> Subquery Scan on s1
+ -> Nested Loop Semi Join
+ Join Filter: (s1_1.a = s2_2.x)
+ -> Seq Scan on s1 s1_1
+ -> Materialize
+ -> Subquery Scan on s2_2
+ Filter: (s2_2.y ~~ '%af%'::text)
+ -> Seq Scan on s2 s2_3
+ Filter: ((x % 2) = 0)
+(15 rows)
+
+ALTER TABLE s2 SET ROW SECURITY FOR ALL
+ TO (x in (select a from s1 where b like '%d2%'));
+SELECT * FROM s1 WHERE f_leak(b); -- fail (infinite recursion via view)
+ERROR: infinite recursion detected for relation "s1"
+-- prepared statement with rls_regress_user0 privilege
+PREPARE p1(int) AS SELECT * FROM t1 WHERE a <= $1;
+EXECUTE p1(2);
+ a | b
+---+-----
+ 2 | bbb
+ 1 | abc
+ 1 | xxx
+ 2 | yyy
+(4 rows)
+
+EXPLAIN (costs off) EXECUTE p1(2);
+ QUERY PLAN
+----------------------------------------------------
+ Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a <= 2) AND ((a % 2) = 0))
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a <= 2) AND ((a % 2) = 1))
+ -> Seq Scan on t3
+ Filter: (a <= 2)
+(9 rows)
+
+-- superuser is allowed to bypass RLS checks
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1 WHERE f_leak(b);
+NOTICE: f_leak => aaa
+NOTICE: f_leak => bbb
+NOTICE: f_leak => ccc
+NOTICE: f_leak => ddd
+NOTICE: f_leak => abc
+NOTICE: f_leak => bcd
+NOTICE: f_leak => cde
+NOTICE: f_leak => def
+NOTICE: f_leak => xxx
+NOTICE: f_leak => yyy
+NOTICE: f_leak => zzz
+ a | b
+---+-----
+ 1 | aaa
+ 2 | bbb
+ 3 | ccc
+ 4 | ddd
+ 1 | abc
+ 2 | bcd
+ 3 | cde
+ 4 | def
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(11 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+ QUERY PLAN
+---------------------------
+ Append
+ -> Seq Scan on t1
+ Filter: f_leak(b)
+ -> Seq Scan on t2
+ Filter: f_leak(b)
+ -> Seq Scan on t3
+ Filter: f_leak(b)
+(7 rows)
+
+-- plan cache should be invalidated
+EXECUTE p1(2);
+ a | b
+---+-----
+ 1 | aaa
+ 2 | bbb
+ 1 | abc
+ 2 | bcd
+ 1 | xxx
+ 2 | yyy
+(6 rows)
+
+EXPLAIN (costs off) EXECUTE p1(2);
+ QUERY PLAN
+--------------------------
+ Append
+ -> Seq Scan on t1
+ Filter: (a <= 2)
+ -> Seq Scan on t2
+ Filter: (a <= 2)
+ -> Seq Scan on t3
+ Filter: (a <= 2)
+(7 rows)
+
+PREPARE p2(int) AS SELECT * FROM t1 WHERE a = $1;
+EXECUTE p2(2);
+ a | b
+---+-----
+ 2 | bbb
+ 2 | bcd
+ 2 | yyy
+(3 rows)
+
+EXPLAIN (costs off) EXECUTE p2(2);
+ QUERY PLAN
+-------------------------
+ Append
+ -> Seq Scan on t1
+ Filter: (a = 2)
+ -> Seq Scan on t2
+ Filter: (a = 2)
+ -> Seq Scan on t3
+ Filter: (a = 2)
+(7 rows)
+
+-- also, case when privilege switch from superuser
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXECUTE p2(2);
+ a | b
+---+-----
+ 2 | bbb
+ 2 | yyy
+(2 rows)
+
+EXPLAIN (costs off) EXECUTE p2(2);
+ QUERY PLAN
+---------------------------------------------------
+ Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a = 2) AND ((a % 2) = 0))
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a = 2) AND ((a % 2) = 1))
+ -> Seq Scan on t3
+ Filter: (a = 2)
+(9 rows)
+
+--
+-- UPDATE / DELETE and Row-level security
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXPLAIN (costs off) UPDATE t1 SET b = b || b WHERE f_leak(b);
+ QUERY PLAN
+-------------------------------------
+ Update on t1
+ -> Subquery Scan on t1_1
+ Filter: f_leak(t1_1.b)
+ -> Seq Scan on t1 t1_2
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ Filter: f_leak(t2.b)
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+ Filter: f_leak(b)
+(11 rows)
+
+UPDATE t1 SET b = b || b WHERE f_leak(b);
+NOTICE: f_leak => bbb
+NOTICE: f_leak => ddd
+NOTICE: f_leak => abc
+NOTICE: f_leak => cde
+NOTICE: f_leak => xxx
+NOTICE: f_leak => yyy
+NOTICE: f_leak => zzz
+EXPLAIN (costs off) UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b);
+ QUERY PLAN
+-------------------------------------
+ Update on t1
+ -> Subquery Scan on t1_1
+ Filter: f_leak(t1_1.b)
+ -> Seq Scan on t1 t1_2
+ Filter: ((a % 2) = 0)
+(5 rows)
+
+UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b);
+NOTICE: f_leak => bbbbbb
+NOTICE: f_leak => dddddd
+-- returning clause with system column
+UPDATE only t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+NOTICE: f_leak => bbbbbb_updt
+NOTICE: f_leak => dddddd_updt
+ oid | a | b | t1
+-----+---+-------------+-----------------
+ 102 | 2 | bbbbbb_updt | (2,bbbbbb_updt)
+ 104 | 4 | dddddd_updt | (4,dddddd_updt)
+(2 rows)
+
+UPDATE t1 SET b = b WHERE f_leak(b) RETURNING *;
+NOTICE: f_leak => bbbbbb_updt
+NOTICE: f_leak => dddddd_updt
+NOTICE: f_leak => abcabc
+NOTICE: f_leak => cdecde
+NOTICE: f_leak => xxxxxx
+NOTICE: f_leak => yyyyyy
+NOTICE: f_leak => zzzzzz
+ a | b
+---+-------------
+ 2 | bbbbbb_updt
+ 4 | dddddd_updt
+ 1 | abcabc
+ 3 | cdecde
+ 1 | xxxxxx
+ 2 | yyyyyy
+ 3 | zzzzzz
+(7 rows)
+
+UPDATE t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+NOTICE: f_leak => bbbbbb_updt
+NOTICE: f_leak => dddddd_updt
+NOTICE: f_leak => abcabc
+NOTICE: f_leak => cdecde
+NOTICE: f_leak => xxxxxx
+NOTICE: f_leak => yyyyyy
+NOTICE: f_leak => zzzzzz
+ oid | a | b | t1
+-----+---+-------------+-----------------
+ 102 | 2 | bbbbbb_updt | (2,bbbbbb_updt)
+ 104 | 4 | dddddd_updt | (4,dddddd_updt)
+ 201 | 1 | abcabc | (1,abcabc)
+ 203 | 3 | cdecde | (3,cdecde)
+ 301 | 1 | xxxxxx | (1,xxxxxx)
+ 302 | 2 | yyyyyy | (2,yyyyyy)
+ 303 | 3 | zzzzzz | (3,zzzzzz)
+(7 rows)
+
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1;
+ a | b
+---+-------------
+ 1 | aaa
+ 3 | ccc
+ 2 | bbbbbb_updt
+ 4 | dddddd_updt
+ 2 | bcd
+ 4 | def
+ 1 | abcabc
+ 3 | cdecde
+ 1 | xxxxxx
+ 2 | yyyyyy
+ 3 | zzzzzz
+(11 rows)
+
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXPLAIN (costs off) DELETE FROM only t1 WHERE f_leak(b);
+ QUERY PLAN
+-------------------------------------
+ Delete on t1
+ -> Subquery Scan on t1_1
+ Filter: f_leak(t1_1.b)
+ -> Seq Scan on t1 t1_2
+ Filter: ((a % 2) = 0)
+(5 rows)
+
+EXPLAIN (costs off) DELETE FROM t1 WHERE f_leak(b);
+ QUERY PLAN
+-------------------------------------
+ Delete on t1
+ -> Subquery Scan on t1_1
+ Filter: f_leak(t1_1.b)
+ -> Seq Scan on t1 t1_2
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ Filter: f_leak(t2.b)
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+ Filter: f_leak(b)
+(11 rows)
+
+DELETE FROM only t1 WHERE f_leak(b) RETURNING oid, *, t1;
+NOTICE: f_leak => bbbbbb_updt
+NOTICE: f_leak => dddddd_updt
+ oid | a | b | t1
+-----+---+-------------+-----------------
+ 102 | 2 | bbbbbb_updt | (2,bbbbbb_updt)
+ 104 | 4 | dddddd_updt | (4,dddddd_updt)
+(2 rows)
+
+DELETE FROM t1 WHERE f_leak(b) RETURNING oid, *, t1;
+NOTICE: f_leak => abcabc
+NOTICE: f_leak => cdecde
+NOTICE: f_leak => xxxxxx
+NOTICE: f_leak => yyyyyy
+NOTICE: f_leak => zzzzzz
+ oid | a | b | t1
+-----+---+--------+------------
+ 201 | 1 | abcabc | (1,abcabc)
+ 203 | 3 | cdecde | (3,cdecde)
+ 301 | 1 | xxxxxx | (1,xxxxxx)
+ 302 | 2 | yyyyyy | (2,yyyyyy)
+ 303 | 3 | zzzzzz | (3,zzzzzz)
+(5 rows)
+
+--
+-- Test psql \dt+ command
+--
+ALTER TABLE category RESET ROW SECURITY FOR ALL; -- too long qual
+\dt+
+ List of relations
+ Schema | Name | Type | Owner | Size | Description | Row-security
+--------------------+----------+-------+-------------------+------------+-------------+----------------------------------
+ rls_regress_schema | category | table | rls_regress_user0 | 16 kB | |
+ rls_regress_schema | document | table | rls_regress_user0 | 16 kB | | (dauthor = "current_user"())
+ rls_regress_schema | s1 | table | rls_regress_user0 | 16 kB | | (a IN ( SELECT v2.x +
+ | | | | | | FROM v2))
+ rls_regress_schema | s2 | table | rls_regress_user0 | 16 kB | | (x IN ( SELECT s1.a +
+ | | | | | | FROM s1 +
+ | | | | | | WHERE (s1.b ~~ '%d2%'::text)))
+ rls_regress_schema | t1 | table | rls_regress_user0 | 16 kB | | ((a % 2) = 0)
+ rls_regress_schema | t2 | table | rls_regress_user0 | 16 kB | | ((a % 2) = 1)
+ rls_regress_schema | t3 | table | rls_regress_user0 | 16 kB | |
+ rls_regress_schema | uaccount | table | rls_regress_user0 | 8192 bytes | |
+(8 rows)
+
+--
+-- Clean up objects
+--
+RESET SESSION AUTHORIZATION;
+DROP SCHEMA rls_regress_schema CASCADE;
+NOTICE: drop cascades to 10 other objects
+DETAIL: drop cascades to function f_leak(text)
+drop cascades to table uaccount
+drop cascades to table category
+drop cascades to table document
+drop cascades to table t1
+drop cascades to table t2
+drop cascades to table t3
+drop cascades to table s1
+drop cascades to table s2
+drop cascades to view v2
+DROP USER rls_regress_user0;
+DROP USER rls_regress_user1;
+DROP USER rls_regress_user2;
diff --git a/src/test/regress/expected/sanity_check.out b/src/test/regress/expected/sanity_check.out
index 432d39a..3b9123d 100644
--- a/src/test/regress/expected/sanity_check.out
+++ b/src/test/regress/expected/sanity_check.out
@@ -120,6 +120,7 @@ SELECT relname, relhasindex
pg_proc | t
pg_range | t
pg_rewrite | t
+ pg_rowsecurity | t
pg_seclabel | t
pg_shdepend | t
pg_shdescription | t
@@ -166,7 +167,7 @@ SELECT relname, relhasindex
timetz_tbl | f
tinterval_tbl | f
varchar_tbl | f
-(155 rows)
+(156 rows)
--
-- another sanity check: every system catalog that has OIDs should have
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 2af28b1..3a36bce 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 matview
+test: privileges rowsecurity security_label collate matview
# ----------
# Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index d6eaa7a..9f52f6a 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -93,6 +93,7 @@ test: delete
test: namespace
test: prepared_xacts
test: privileges
+test: rowsecurity
test: security_label
test: collate
test: matview
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
new file mode 100644
index 0000000..55d4aad
--- /dev/null
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -0,0 +1,298 @@
+--
+-- Test of Row-level security feature
+--
+
+-- Clean up in case a prior regression run failed
+
+-- Suppress NOTICE messages when users/groups don't exist
+SET client_min_messages TO 'warning';
+
+DROP USER IF EXISTS rls_regress_user0;
+DROP USER IF EXISTS rls_regress_user1;
+DROP USER IF EXISTS rls_regress_user2;
+
+DROP SCHEMA IF EXISTS rls_regress_schema CASCADE;
+
+RESET client_min_messages;
+
+-- initial setup
+CREATE USER rls_regress_user0;
+CREATE USER rls_regress_user1;
+CREATE USER rls_regress_user2;
+
+CREATE SCHEMA rls_regress_schema;
+GRANT ALL ON SCHEMA rls_regress_schema TO public;
+SET search_path = rls_regress_schema;
+
+-- setup of malicious function
+CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool
+ COST 0.0000001 LANGUAGE plpgsql
+ AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END';
+GRANT EXECUTE ON FUNCTION f_leak(text) TO public;
+
+-- BASIC Row-Level Security Scenario
+
+SET SESSION AUTHORIZATION rls_regress_user0;
+CREATE TABLE uaccount (
+ pguser name primary key,
+ seclv int
+);
+INSERT INTO uaccount VALUES
+ ('rls_regress_user0', 99),
+ ('rls_regress_user1', 1),
+ ('rls_regress_user2', 2),
+ ('rls_regress_user3', 3);
+GRANT SELECT ON uaccount TO public;
+
+CREATE TABLE category (
+ cid int primary key,
+ cname text
+);
+GRANT ALL ON category TO public;
+INSERT INTO category VALUES
+ (11, 'novel'),
+ (22, 'science fiction'),
+ (33, 'technology'),
+ (44, 'manga');
+
+CREATE TABLE document (
+ did int primary key,
+ cid int references category(cid),
+ dlevel int not null,
+ dauthor name,
+ dtitle text
+);
+GRANT ALL ON document TO public;
+INSERT INTO document VALUES
+ ( 1, 11, 1, 'rls_regress_user1', 'my first novel'),
+ ( 2, 11, 2, 'rls_regress_user1', 'my second novel'),
+ ( 3, 22, 2, 'rls_regress_user1', 'my science fiction'),
+ ( 4, 44, 1, 'rls_regress_user1', 'my first manga'),
+ ( 5, 44, 2, 'rls_regress_user1', 'my second manga'),
+ ( 6, 22, 1, 'rls_regress_user2', 'great science fiction'),
+ ( 7, 33, 2, 'rls_regress_user2', 'great technology book'),
+ ( 8, 44, 1, 'rls_regress_user2', 'great manga');
+
+-- user's security level must higher than or equal to document's one
+ALTER TABLE document SET ROW SECURITY FOR ALL
+ TO (dlevel <= (SELECT seclv FROM uaccount WHERE pguser = current_user));
+
+-- viewpoint from rls_regress_user1
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+-- viewpoint from rls_regress_user2
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+-- only owner can change row-level security
+ALTER TABLE document SET ROW SECURITY FOR ALL TO (true); -- fail
+ALTER TABLE document RESET ROW SECURITY FOR ALL; -- fail
+
+SET SESSION AUTHORIZATION rls_regress_user0;
+ALTER TABLE document SET ROW SECURITY FOR ALL TO (dauthor = current_user);
+
+-- viewpoint from rls_regress_user1 again
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+-- viewpoint from rls_regress_user2 again
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+-- interaction of FK/PK constraints
+SET SESSION AUTHORIZATION rls_regress_user0;
+ALTER TABLE category SET ROW SECURITY FOR ALL
+ TO (CASE WHEN current_user = 'rls_regress_user1' THEN cid IN (11, 33)
+ WHEN current_user = 'rls_regress_user2' THEN cid IN (22, 44)
+ ELSE false END);
+
+-- cannot delete PK referenced by invisible FK
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document d full outer join category c on d.cid = c.cid;
+DELETE FROM category WHERE cid = 33; -- failed
+
+-- cannot insert FK referencing invisible PK
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document d full outer join category c on d.cid = c.cid;
+INSERT INTO document VALUES (10, 33, 1, current_user, 'hoge'); -- failed
+
+-- database superuser can bypass RLS policy
+RESET SESSION AUTHORIZATION;
+SELECT * FROM document;
+SELECT * FROM category;
+
+--
+-- Table inheritance and RLS policy
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+
+CREATE TABLE t1 (a int, junk1 text, b text) WITH OIDS;
+ALTER TABLE t1 DROP COLUMN junk1; -- just a disturbing factor
+GRANT ALL ON t1 TO public;
+
+COPY t1 FROM stdin WITH (oids);
+101 1 aaa
+102 2 bbb
+103 3 ccc
+104 4 ddd
+\.
+
+CREATE TABLE t2 (c float) INHERITS (t1);
+COPY t2 FROM stdin WITH (oids);
+201 1 abc 1.1
+202 2 bcd 2.2
+203 3 cde 3.3
+204 4 def 4.4
+\.
+
+CREATE TABLE t3 (c text, b text, a int) WITH OIDS;
+ALTER TABLE t3 INHERIT t1;
+COPY t3(a,b,c) FROM stdin WITH (oids);
+301 1 xxx X
+302 2 yyy Y
+303 3 zzz Z
+\.
+
+ALTER TABLE t1 SET ROW SECURITY FOR ALL TO (a % 2 = 0); -- be even number
+ALTER TABLE t2 SET ROW SECURITY FOR ALL TO (a % 2 = 1); -- be odd number
+
+SELECT * FROM t1;
+EXPLAIN (costs off) SELECT * FROM t1;
+
+SELECT * FROM t1 WHERE f_leak(b);
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+
+-- reference to system column
+SELECT oid, * FROM t1;
+EXPLAIN (costs off) SELECT * FROM t1;
+
+-- reference to whole-row reference
+SELECT *,t1 FROM t1;
+EXPLAIN (costs off) SELECT *,t1 FROM t1;
+
+-- for share/update lock
+SELECT * FROM t1 FOR SHARE;
+EXPLAIN (costs off) SELECT * FROM t1 FOR SHARE;
+
+SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+
+--
+-- COPY TO statement
+--
+COPY t1 TO stdout;
+COPY t1 TO stdout WITH OIDS;
+COPY t2(c,b) TO stdout WITH OIDS;
+COPY (SELECT * FROM t1) TO stdout;
+COPY document TO stdout WITH OIDS; -- failed (no oid column)
+
+--
+-- recursive RLS and VIEWs in policy
+--
+CREATE TABLE s1 (a int, b text);
+INSERT INTO s1 (SELECT x, md5(x::text) FROM generate_series(-10,10) x);
+
+CREATE TABLE s2 (x int, y text);
+INSERT INTO s2 (SELECT x, md5(x::text) FROM generate_series(-6,6) x);
+CREATE VIEW v2 AS SELECT * FROM s2 WHERE y like '%af%';
+
+ALTER TABLE s1 SET ROW SECURITY FOR ALL
+ TO (a in (select x from s2 where y like '%2f%'));
+
+ALTER TABLE s2 SET ROW SECURITY FOR ALL
+ TO (x in (select a from s1 where b like '%22%'));
+
+SELECT * FROM s1 WHERE f_leak(b); -- fail (infinite recursion)
+
+ALTER TABLE s2 SET ROW SECURITY FOR ALL TO (x % 2 = 0);
+
+SELECT * FROM s1 WHERE f_leak(b); -- OK
+EXPLAIN SELECT * FROM only s1 WHERE f_leak(b);
+
+ALTER TABLE s1 SET ROW SECURITY FOR ALL
+ TO (a in (select x from v2)); -- using VIEW in RLS policy
+SELECT * FROM s1 WHERE f_leak(b); -- OK
+EXPLAIN (COSTS OFF) SELECT * FROM s1 WHERE f_leak(b);
+
+SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+EXPLAIN (COSTS OFF) SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+
+ALTER TABLE s2 SET ROW SECURITY FOR ALL
+ TO (x in (select a from s1 where b like '%d2%'));
+SELECT * FROM s1 WHERE f_leak(b); -- fail (infinite recursion via view)
+
+-- prepared statement with rls_regress_user0 privilege
+PREPARE p1(int) AS SELECT * FROM t1 WHERE a <= $1;
+EXECUTE p1(2);
+EXPLAIN (costs off) EXECUTE p1(2);
+
+-- superuser is allowed to bypass RLS checks
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1 WHERE f_leak(b);
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+
+-- plan cache should be invalidated
+EXECUTE p1(2);
+EXPLAIN (costs off) EXECUTE p1(2);
+
+PREPARE p2(int) AS SELECT * FROM t1 WHERE a = $1;
+EXECUTE p2(2);
+EXPLAIN (costs off) EXECUTE p2(2);
+
+-- also, case when privilege switch from superuser
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXECUTE p2(2);
+EXPLAIN (costs off) EXECUTE p2(2);
+
+--
+-- UPDATE / DELETE and Row-level security
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXPLAIN (costs off) UPDATE t1 SET b = b || b WHERE f_leak(b);
+UPDATE t1 SET b = b || b WHERE f_leak(b);
+
+EXPLAIN (costs off) UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b);
+UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b);
+
+-- returning clause with system column
+UPDATE only t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+UPDATE t1 SET b = b WHERE f_leak(b) RETURNING *;
+UPDATE t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1;
+
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXPLAIN (costs off) DELETE FROM only t1 WHERE f_leak(b);
+EXPLAIN (costs off) DELETE FROM t1 WHERE f_leak(b);
+
+DELETE FROM only t1 WHERE f_leak(b) RETURNING oid, *, t1;
+DELETE FROM t1 WHERE f_leak(b) RETURNING oid, *, t1;
+
+--
+-- Test psql \dt+ command
+--
+ALTER TABLE category RESET ROW SECURITY FOR ALL; -- too long qual
+\dt+
+
+--
+-- Clean up objects
+--
+RESET SESSION AUTHORIZATION;
+
+DROP SCHEMA rls_regress_schema CASCADE;
+
+DROP USER rls_regress_user0;
+DROP USER rls_regress_user1;
+DROP USER rls_regress_user2;
Hackers,
Please, oh please, won't someone review this? This patch has been 3
years in the making, so I suspect that it will NOT be a fast review.
--
Josh Berkus
PostgreSQL Experts Inc.
http://pgexperts.com
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Import Notes
Reply to msg id not found: WMd35eac1385c74af30d4dfbce89dd77c7fc78330699e900f9e0be8086ec797440d7929152b0ce0fc4c3fdba743ff00dee@asav-3.01.com
I was pointed out that the previous patch was not applied cleanly because of
enhancement on pg_class system catalog, and related pg_dump portion was
getting broken.
The attached patch fixes this matters. Please reference this patch instead.
Thanks,
2013/6/13 Kohei KaiGai <kaigai@kaigai.gr.jp>:
The attached patch implements row-level security feature; that allows to
enforce a pre-configured security policy on reference of tables with the
row-level security policy.
It enables to isolate records to be visible from others according to access
control decision, usually done based on current user's credential.
It will make sense to ensure correctness of security for SaaS style
applications that typically share a common table for multiple users but
correctness of access control was ensured with correctness of application
itself.Here is not functional update since the last commit fest for v9.3 except
for adjustment towards the latest master branch.So, the explanation below might be bored for someone.
This feature enhances ALTER TABLE statement as follows:
ALTER TABLE <tablename> SET ROW SECURITY
FOR <command> TO (<expression>);
ALTER TABLE <tablename> RESET ROW SECURITY
FOR <command>;
<command> := { ALL | SELECT | INSERT | UPDATE | DELETE }Right now, only "ALL" is supported command, even though syntax reserves
future enhancement allows to set individual security policy for each command.
The <expression> should be an expression that returns a bool value. It can
reference any column in the target table and contain sub-query that reference
another tables.
Then, the pre-configured expression shall be added when the table is referenced.See below, it gives "(X % 2 = 1)" as security policy, user can see the record
that has odd-number at X. The EXPLAIN output below shows this expression
was automatically attached.postgres=> ALTER TABLE tbl SET ROW SECURITY FOR ALL TO (x % 2 = 1);
ALTER TABLE
postgres=> EXPLAIN SELECT * FROM tbl WHERE y like '%abc%';
QUERY PLAN
-----------------------------------------------------------------
Subquery Scan on tbl (cost=0.00..28.52 rows=1 width=36)
Filter: (tbl.y ~~ '%abc%'::text)
-> Seq Scan on tbl tbl_1 (cost=0.00..28.45 rows=6 width=36)
Filter: ((x % 2) = 1)
(4 rows)An important point is, reference to a particular relation is replaced
with a sub-
query that has security policy expression and security barrier attribute.
That prevent any (non leakproof) user given condition earlier than
security poliy
itself, thus, it ensures all records user can see is satisfies the
security policy.On writer-queries, things to do are similar. It adds security policy expression
on the scan phase of UPDATE or DELETE statement. Thus, only visible records
are updatable or deletable.postgres=> EXPLAIN UPDATE tbl SET y = y || '_update' WHERE y like '%xyz%';
QUERY PLAN
-----------------------------------------------------------------------
Update on tbl (cost=0.00..28.53 rows=1 width=42)
-> Subquery Scan on tbl_1 (cost=0.00..28.53 rows=1 width=42)
Filter: (tbl_1.y ~~ '%xyz%'::text)
-> Seq Scan on tbl tbl_2 (cost=0.00..28.45 rows=6 width=42)
Filter: ((x % 2) = 1)
(5 rows)I had a relevant presentation at PGcon last month. I think its slides
are good summary
to know brief background of the long-standing problem.
http://www.pgcon.org/2013/schedule/attachments/273_PGcon2013-kaigai-row-level-security.pdfThanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
Attachments:
pgsql-v9.4-row-level-security.v3.patchapplication/octet-stream; name=pgsql-v9.4-row-level-security.v3.patchDownload
doc/src/sgml/catalogs.sgml | 65 ++
doc/src/sgml/ref/alter_table.sgml | 43 ++
doc/src/sgml/user-manag.sgml | 145 +++++
src/backend/catalog/Makefile | 4 +-
src/backend/catalog/dependency.c | 8 +
src/backend/catalog/heap.c | 1 +
src/backend/catalog/objectaddress.c | 50 ++
src/backend/catalog/pg_rowsecurity.c | 337 ++++++++++
src/backend/commands/copy.c | 90 ++-
src/backend/commands/event_trigger.c | 1 +
src/backend/commands/tablecmds.c | 27 +
src/backend/nodes/copyfuncs.c | 3 +
src/backend/nodes/equalfuncs.c | 3 +
src/backend/nodes/nodeFuncs.c | 12 +-
src/backend/nodes/outfuncs.c | 3 +
src/backend/nodes/readfuncs.c | 2 +
src/backend/optimizer/plan/planner.c | 23 +-
src/backend/optimizer/prep/preptlist.c | 61 +-
src/backend/optimizer/prep/prepunion.c | 95 ++-
src/backend/optimizer/util/Makefile | 2 +-
src/backend/optimizer/util/rowsecurity.c | 733 ++++++++++++++++++++++
src/backend/parser/gram.y | 25 +
src/backend/parser/parse_agg.c | 6 +
src/backend/parser/parse_expr.c | 3 +
src/backend/parser/parse_relation.c | 19 +
src/backend/rewrite/rewriteHandler.c | 16 +
src/backend/utils/adt/ri_triggers.c | 13 +-
src/backend/utils/cache/plancache.c | 32 +
src/backend/utils/cache/relcache.c | 17 +-
src/bin/pg_dump/common.c | 4 +
src/bin/pg_dump/pg_backup_archiver.c | 1 +
src/bin/pg_dump/pg_dump.c | 223 ++++++-
src/bin/pg_dump/pg_dump.h | 13 +-
src/bin/pg_dump/pg_dump_sort.c | 5 +
src/bin/psql/describe.c | 7 +
src/include/catalog/dependency.h | 1 +
src/include/catalog/indexing.h | 5 +
src/include/catalog/pg_class.h | 24 +-
src/include/catalog/pg_rowsecurity.h | 76 +++
src/include/miscadmin.h | 1 +
src/include/nodes/execnodes.h | 4 +
src/include/nodes/nodeFuncs.h | 1 +
src/include/nodes/parsenodes.h | 14 +-
src/include/nodes/plannodes.h | 2 +
src/include/nodes/relation.h | 6 +
src/include/optimizer/rowsecurity.h | 27 +
src/include/parser/parse_node.h | 3 +-
src/include/parser/parsetree.h | 5 +-
src/include/rewrite/rewriteHandler.h | 1 +
src/include/utils/plancache.h | 2 +
src/include/utils/rel.h | 2 +
src/test/regress/expected/rowsecurity.out | 950 +++++++++++++++++++++++++++++
src/test/regress/expected/sanity_check.out | 3 +-
src/test/regress/parallel_schedule | 2 +-
src/test/regress/serial_schedule | 1 +
src/test/regress/sql/rowsecurity.sql | 298 +++++++++
56 files changed, 3477 insertions(+), 43 deletions(-)
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 6715782..6f3c8fc 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -234,6 +234,11 @@
</row>
<row>
+ <entry><link linkend="catalog-pg-rowlevelsec"><structname>pg_rowlevelsec</structname></link></entry>
+ <entry>row-level security policy of relation</entry>
+ </row>
+
+ <row>
<entry><link linkend="catalog-pg-seclabel"><structname>pg_seclabel</structname></link></entry>
<entry>security labels on database objects</entry>
</row>
@@ -1848,6 +1853,16 @@
</row>
<row>
+ <entry><structfield>relhasrowsecurity</structfield></entry>
+ <entry><type>bool</type></entry>
+ <entry></entry>
+ <entry>
+ True if table has row-security policy; see
+ <link linkend="catalog-pg-rowsecurity"><structname>pg_rowsecurity</structname></link> catalog
+ </entry>
+ </row>
+
+ <row>
<entry><structfield>relhassubclass</structfield></entry>
<entry><type>bool</type></entry>
<entry></entry>
@@ -5112,6 +5127,56 @@
</sect1>
+ <sect1 id="catalog-pg-rowsecurity">
+ <title><structname>pg_rowlevelsec</structname></title>
+
+ <indexterm zone="catalog-pg-rowsecurity">
+ <primary>pg_rowsecurity</primary>
+ </indexterm>
+ <para>
+ The catalog <structname>pg_rowsecurity</structname> stores expression
+ tree of row-security policy to be performed on a particular relation.
+ </para>
+ <table>
+ <title><structname>pg_rowsecurity</structname> Columns</title>
+ <tgroup cols="4">
+ <thead>
+ <row>
+ <entry>Name</entry>
+ <entry>Type</entry>
+ <entry>References</entry>
+ <entry>Description</entry>
+ </row>
+ </thead>
+ <tbody>
+ <row>
+ <entry><structfield>rsecrelid</structfield></entry>
+ <entry><type>oid</type></entry>
+ <entry><literal><link linkend="catalog-pg-class"><structname>pg_class</structname></link>.oid</literal></entry>
+ <entry>The table this row-security is for</entry>
+ </row>
+ <row>
+ <entry><structfield>rseccmd</structfield></entry>
+ <entry><type>char</type></entry>
+ <entry></entry>
+ <entry>The command this row-security is for. 'a' meaning all is only possible value right now.</entry>
+ </row>
+ <row>
+ <entry><structfield>rsecqual</structfield></entry>
+ <entry><type>pg_node_tree</type></entry>
+ <entry></entry>
+ <entry>An expression tree to be performed as rowl-security policy</entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+ <note>
+ <para>
+ <literal>pg_class.relhasrowlevelsec</literal>
+ must be true if a table has row-level security policy in this catalog.
+ </para>
+ </note>
+ </sect1>
<sect1 id="catalog-pg-seclabel">
<title><structname>pg_seclabel</structname></title>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 2609d4a..b380852 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -69,12 +69,16 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable>
NOT OF
OWNER TO <replaceable class="PARAMETER">new_owner</replaceable>
SET TABLESPACE <replaceable class="PARAMETER">new_tablespace</replaceable>
+ SET ROW SECURITY FOR <replaceable class="PARAMETER">rowsec_command</replaceable> TO (<replaceable class="PARAMETER">condition</replaceable>)
+ RESET ROW SECURITY FOR <replaceable class="PARAMETER">rowsec_command</replaceable>
<phrase>and <replaceable class="PARAMETER">table_constraint_using_index</replaceable> is:</phrase>
[ CONSTRAINT <replaceable class="PARAMETER">constraint_name</replaceable> ]
{ UNIQUE | PRIMARY KEY } USING INDEX <replaceable class="PARAMETER">index_name</replaceable>
[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+<phrase>and <replaceable class="PARAMETER">rowsec_command</replaceable> is:</phrase>
+ { ALL | SELECT | INSERT | UPDATE | DELETE }
</synopsis>
</refsynopsisdiv>
@@ -580,6 +584,31 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable>
</varlistentry>
<varlistentry>
+ <term><literal>SET ROW SECURITY FOR <replaceable class="PARAMETER">rowsec_command</replaceable> TO (<replaceable class="PARAMETER">condition</replaceable>)</literal></term>
+ <listitem>
+ <para>
+ This form set row-level security policy of the table.
+ Supplied <replaceable class="PARAMETER">condition</replaceable> performs
+ as if it is implicitly appended to the qualifiers of <literal>WHERE</literal>
+ clause, although mechanism guarantees to evaluate this condition earlier
+ than any other user given condition.
+ <literal>ALL</> is the only supported command type right now.
+ See also <xref linkend="ROW-SECURITY">.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>RESET ROW SECURITY FOR <replaceable class="PARAMETER">rowsec_command</replaceable></literal></term>
+ <listitem>
+ <para>
+ This form reset row-level security policy of the table, if exists.
+ <literal>ALL</> is the only supported command type right now.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
<term><literal>RENAME</literal></term>
<listitem>
<para>
@@ -821,6 +850,20 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable>
</listitem>
</varlistentry>
+ <varlistentry>
+ <term><replaceable class="PARAMETER">condition</replaceable></term>
+ <listitem>
+ <para>
+ An expression that returns a value of type boolean. Expect for a case
+ when queries are executed with superuser privilege, only rows for which
+ this expression returns true will be fetched, updated or deleted.
+ This expression can reference columns of the relation being configured.
+ Sub-queries can be contained within expression tree, unless referenced
+ relation recursively references the same relation.
+ </para>
+ </listitem>
+ </varlistentry>
+
</variablelist>
</refsect1>
diff --git a/doc/src/sgml/user-manag.sgml b/doc/src/sgml/user-manag.sgml
index 177ac7a..34a699e 100644
--- a/doc/src/sgml/user-manag.sgml
+++ b/doc/src/sgml/user-manag.sgml
@@ -439,4 +439,149 @@ DROP ROLE <replaceable>name</replaceable>;
</para>
</sect1>
+ <sect1 id="row-security">
+ <title>Row-security</title>
+ <para>
+ <productname>PostgreSQL</productname> v9.3 or later provides
+ row-security feature, like several commercial database
+ management system. It allows table owner to assign a particular
+ condition that performs as a security policy of the table; only
+ rows that satisfies the condition should be visible, except for
+ a case when superuser runs queries.
+ </para>
+ <para>
+ Row-security policy can be set using <literal>SET ROW SECURITY</literal>
+ command of <xref linkend="SQL-ALTERTABLE"> 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.
+ </para>
+ <para>
+ For example, the following <literal>customer</literal> table has
+ <literal>uname</literal> field to store user name, and it assume
+ we don't want to expose any properties of other customers.
+ The following command set <literal>current_user = uname</literal>
+ as row-security policy on the <literal>customer</literal> table.
+<screen>
+postgres=> ALTER TABLE customer SET ROW SECURITY
+ FOR ALL TO (current_user = uname);
+ALTER TABLE
+</screen>
+ <xref linkend="SQL-EXPLAIN"> command shows how row-security policy
+ works on the supplied query.
+<screen>
+postgres=> EXPLAIN(costs off) SELECT * FROM customer WHERE f_leak(upasswd);
+ QUERY PLAN
+--------------------------------------------
+ Subquery Scan on customer
+ Filter: f_leak(customer.upasswd)
+ -> Seq Scan on customer customer_1
+ Filter: ("current_user"() = uname)
+(4 rows)
+</screen>
+ This query execution plan means the preconfigured row-security policy is
+ implicitly added, and scan plan on the target relation being wrapped up
+ with a sub-query.
+ It ensures user given qualifiers, including functions with side effects,
+ are never executed earlier than the row-security policy regardless of
+ its cost, except for the cases when these were fully leakproof.
+ This design helps to tackle the scenario described in
+ <xref linkend="RULES-PRIVILEGES">; that introduces the order to evaluate
+ qualifiers is significant to keep confidentiality of invisible rows.
+ </para>
+
+ <para>
+ On the other hand, this design allows superusers to bypass checks with
+ row-security.
+ It ensures <application>pg_dump</application> can obtain a complete set
+ of database backup, and avoid to execute Trojan horse trap, being injected
+ as a row-security policy of user-defined table, with privileges of
+ superuser.
+ </para>
+
+ <para>
+ In case of queries on inherited tables, row-security policy of the parent
+ relation is not applied to child relations. Scope of the row-security
+ policy is limited to the relation on which it is set.
+<screen>
+postgres=> EXPLAIN(costs off) SELECT * FROM t1 WHERE f_leak(y);
+ QUERY PLAN
+-------------------------------------
+ Append
+ -> Subquery Scan on t1
+ Filter: f_leak(t1.y)
+ -> Seq Scan on t1 t1_1
+ Filter: ((x % 2) = 0)
+ -> Seq Scan on t2
+ Filter: f_leak(y)
+ -> Subquery Scan on t3
+ Filter: f_leak(t3.y)
+ -> Seq Scan on t3 t3_1
+ Filter: ((x % 2) = 1)
+(11 rows)
+</screen>
+ In the above example, <literal>t1</literal> has inherited
+ child table <literal>t2</literal> and <literal>t3</literal>,
+ and row-security policy is set on only <literal>t1</literal>,
+ and <literal>t3</literal>, not <literal>t2</literal>.
+
+ The row-security policy of <literal>t1</literal>, <literal>x</literal>
+ must be even-number, is appended only <literal>t1</literal>,
+ neither <literal>t2</literal> nor <literal>t3</literal>.
+ On the contrary, <literal>t3</literal> has different row-security policy;
+ <literal>x</literal> must be odd-number.
+ </para>
+
+ <para>
+ Row-security feature also works to queries for writer-operations; such as
+ <xref linkend="SQL-INSERT">, <xref linkend="SQL-UPDATE"> or
+ <xref linkend="SQL-DELETE"> commands.
+ It ensures all the modified rows satisfies configured row-security policy.
+ The below query tries to update e-mail address of the
+ <literal>customer</> table, and configured row-security makes sure all the
+ modified rows's <literal>uname</> field has to match with
+ <literal>current_user</>.
+ </para>
+<screen>
+postgres=> EXPLAIN (costs off) UPDATE customer SET email = 'alice@example.com';
+ QUERY PLAN
+--------------------------------------------------
+ Update on customer
+ -> Subquery Scan on customer
+ -> Seq Scan on customer customer_1
+ Filter: ("current_user"() = uname)
+(4 rows)
+</screen>
+ <para>
+ Modification of a certain table is consist of two different stuffs; one
+ is fetch rows to be modified from the result relation (except for
+ <literal>INSERT</> command), second is insertion of rows to the
+ result relation. Usually, before-row trigger or check constraints are
+ run in the second phase. In addition, row-security policy is also checked
+ on this stage, to prevent to insert or update rows with unprivileged
+ values.
+ </para>
+
+ <para>
+ Unlike other commercial database systems, we don't have any plan to allow
+ individual row-security policy for each command type. Even if we want to
+ perform with difference policy between <xref linkend="SQL-SELECT"> and
+ <xref linkend="SQL-DELETE">, <literal>RETURNING</> clause can leak the rows
+ to be invisible using <xref linkend="SQL-DELETE"> command.
+ </para>
+
+ <para>
+ Even though it is not a specific matter in row-security, please be careful
+ if you plan to use <literal>current_user</> in row-level security policy.
+ <xref linkend="SQL-DECLARE"> and <xref linkend="SQL-FETCH"> allows to
+ switch current user identifier during execution of the query.
+ Thus, it can fetch the first 100 rows with privilege of <literal>alice</>,
+ then remaining rows with privilege of <literal>bob</>. If and when query
+ execution plan contains some kind of materialization and row-security
+ policy contains <literal>current_user</>, the fetched tuples in
+ <literal>bob</>'s screen might be evaluated according to the privilege of
+ <literal>alice</>.
+ </para>
+ </sect1>
</chapter>
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index c4d3f3c..9b4e9f5 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -15,7 +15,7 @@ OBJS = catalog.o dependency.o heap.o index.o indexing.o namespace.o aclchk.o \
pg_constraint.o pg_conversion.o \
pg_depend.o pg_enum.o pg_inherits.o pg_largeobject.o pg_namespace.o \
pg_operator.o pg_proc.o pg_range.o pg_db_role_setting.o pg_shdepend.o \
- pg_type.o storage.o toasting.o
+ pg_rowsecurity.o pg_type.o storage.o toasting.o
BKIFILES = postgres.bki postgres.description postgres.shdescription
@@ -39,7 +39,7 @@ POSTGRES_BKI_SRCS = $(addprefix $(top_srcdir)/src/include/catalog/,\
pg_ts_config.h pg_ts_config_map.h pg_ts_dict.h \
pg_ts_parser.h pg_ts_template.h pg_extension.h \
pg_foreign_data_wrapper.h pg_foreign_server.h pg_user_mapping.h \
- pg_foreign_table.h \
+ pg_foreign_table.h pg_rowsecurity.h \
pg_default_acl.h pg_seclabel.h pg_shseclabel.h pg_collation.h pg_range.h \
toasting.h indexing.h \
)
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index fe17c96..d13b2e1 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_rowsecurity.h"
#include "catalog/pg_tablespace.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_ts_config.h"
@@ -1249,6 +1250,10 @@ doDeletion(const ObjectAddress *object, int flags)
RemoveEventTriggerById(object->objectId);
break;
+ case OCLASS_ROWSECURITY:
+ RemoveRowSecurityById(object->objectId);
+ break;
+
default:
elog(ERROR, "unrecognized object class: %u",
object->classId);
@@ -2308,6 +2313,9 @@ getObjectClass(const ObjectAddress *object)
case EventTriggerRelationId:
return OCLASS_EVENT_TRIGGER;
+
+ case RowSecurityRelationId:
+ return OCLASS_ROWSECURITY;
}
/* shouldn't get here */
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 64ca312..7eb7a17 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -791,6 +791,7 @@ InsertPgClassTuple(Relation pg_class_desc,
values[Anum_pg_class_relhaspkey - 1] = BoolGetDatum(rd_rel->relhaspkey);
values[Anum_pg_class_relhasrules - 1] = BoolGetDatum(rd_rel->relhasrules);
values[Anum_pg_class_relhastriggers - 1] = BoolGetDatum(rd_rel->relhastriggers);
+ values[Anum_pg_class_relhasrowsecurity - 1] = BoolGetDatum(rd_rel->relhasrowsecurity);
values[Anum_pg_class_relhassubclass - 1] = BoolGetDatum(rd_rel->relhassubclass);
values[Anum_pg_class_relispopulated - 1] = BoolGetDatum(rd_rel->relispopulated);
values[Anum_pg_class_relfrozenxid - 1] = TransactionIdGetDatum(rd_rel->relfrozenxid);
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 4d22f3a..b4bc9d8 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -2143,6 +2143,56 @@ getObjectDescription(const ObjectAddress *object)
break;
}
+ case OCLASS_ROWSECURITY:
+ {
+ Relation rsec_rel;
+ ScanKeyData skey;
+ SysScanDesc sscan;
+ HeapTuple tuple;
+ Form_pg_rowsecurity form_rsec;
+
+ rsec_rel = heap_open(RowSecurityRelationId, AccessShareLock);
+
+ ScanKeyInit(&skey,
+ ObjectIdAttributeNumber,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(object->objectId));
+ sscan = systable_beginscan(rsec_rel, RowSecurityOidIndexId,
+ true, SnapshotNow, 1, &skey);
+ tuple = systable_getnext(sscan);
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "could not find tuple for row-security %u",
+ object->objectId);
+ form_rsec = (Form_pg_rowsecurity) GETSTRUCT(tuple);
+
+ appendStringInfo(&buffer, _("row-security of "));
+ getRelationDescription(&buffer, form_rsec->rsecrelid);
+ switch (form_rsec->rseccmd)
+ {
+ case ROWSECURITY_CMD_ALL:
+ appendStringInfo(&buffer, _(" FOR ALL"));
+ break;
+ case ROWSECURITY_CMD_SELECT:
+ appendStringInfo(&buffer, _(" FOR SELECT"));
+ break;
+ case ROWSECURITY_CMD_INSERT:
+ appendStringInfo(&buffer, _(" FOR INSERT"));
+ break;
+ case ROWSECURITY_CMD_UPDATE:
+ appendStringInfo(&buffer, _(" FOR UPDATE"));
+ break;
+ case ROWSECURITY_CMD_DELETE:
+ appendStringInfo(&buffer, _(" FOR DELETE"));
+ break;
+ default:
+ elog(ERROR, "unexpected row-security command type: %c",
+ form_rsec->rseccmd);
+ }
+ systable_endscan(sscan);
+ heap_close(rsec_rel, AccessShareLock);
+ break;
+ }
+
default:
appendStringInfo(&buffer, "unrecognized object %u %u %d",
object->classId,
diff --git a/src/backend/catalog/pg_rowsecurity.c b/src/backend/catalog/pg_rowsecurity.c
new file mode 100644
index 0000000..25739ff
--- /dev/null
+++ b/src/backend/catalog/pg_rowsecurity.c
@@ -0,0 +1,337 @@
+/* -------------------------------------------------------------------------
+ *
+ * pg_rowsecurity.c
+ * routines to support manipulation of the pg_rowsecurity catalog
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * -------------------------------------------------------------------------
+ */
+#include "postgres.h"
+#include "access/genam.h"
+#include "access/heapam.h"
+#include "access/htup_details.h"
+#include "access/sysattr.h"
+#include "catalog/dependency.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_rowsecurity.h"
+#include "catalog/pg_type.h"
+#include "nodes/nodeFuncs.h"
+#include "optimizer/clauses.h"
+#include "parser/parse_clause.h"
+#include "parser/parse_node.h"
+#include "parser/parse_relation.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/inval.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/tqual.h"
+
+/*
+ * Load row-security policy from the catalog, and keep it on
+ * the relation cache.
+ */
+void
+RelationBuildRowSecurity(Relation relation)
+{
+ Relation catalog;
+ ScanKeyData skey;
+ SysScanDesc sscan;
+ HeapTuple tuple;
+ MemoryContext oldcxt;
+ MemoryContext rscxt = NULL;
+ RowSecurityDesc *rsdesc = NULL;
+
+ catalog = heap_open(RowSecurityRelationId, AccessShareLock);
+
+ ScanKeyInit(&skey,
+ Anum_pg_rowsecurity_rsecrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(RelationGetRelid(relation)));
+ sscan = systable_beginscan(catalog, RowSecurityRelidIndexId, true,
+ SnapshotNow, 1, &skey);
+ PG_TRY();
+ {
+ while (HeapTupleIsValid(tuple = systable_getnext(sscan)))
+ {
+ Datum value;
+ bool isnull;
+ char *temp;
+
+ if (!rsdesc)
+ {
+ rscxt = AllocSetContextCreate(CacheMemoryContext,
+ "Row-security descriptor",
+ ALLOCSET_SMALL_MINSIZE,
+ ALLOCSET_SMALL_INITSIZE,
+ ALLOCSET_SMALL_MAXSIZE);
+ oldcxt = MemoryContextSwitchTo(rscxt);
+ rsdesc = palloc0(sizeof(RowSecurityDesc));
+ rsdesc->rscxt = rscxt;
+ MemoryContextSwitchTo(oldcxt);
+ }
+ value = heap_getattr(tuple, Anum_pg_rowsecurity_rseccmd,
+ RelationGetDescr(catalog), &isnull);
+ Assert(!isnull);
+
+ if (DatumGetChar(value) != ROWSECURITY_CMD_ALL)
+ {
+ ereport(WARNING,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("Per-command row-security not implemented")));
+ continue;
+ }
+
+ value = heap_getattr(tuple, Anum_pg_rowsecurity_rsecqual,
+ RelationGetDescr(catalog), &isnull);
+ Assert(!isnull);
+ temp = TextDatumGetCString(value);
+
+ oldcxt = MemoryContextSwitchTo(rscxt);
+ rsdesc->rsall.rsecid = HeapTupleGetOid(tuple);
+ rsdesc->rsall.qual = (Expr *) stringToNode(temp);
+ Assert(exprType((Node *)rsdesc->rsall.qual) == BOOLOID);
+ rsdesc->rsall.hassublinks
+ = contain_subplans((Node *)rsdesc->rsall.qual);
+ MemoryContextSwitchTo(oldcxt);
+
+ pfree(temp);
+ }
+ }
+ PG_CATCH();
+ {
+ if (rscxt != NULL)
+ MemoryContextDelete(rscxt);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ systable_endscan(sscan);
+ heap_close(catalog, AccessShareLock);
+
+ relation->rsdesc = rsdesc;
+}
+
+/*
+ * Parse the supplied row-security policy, and insert/update a row
+ * of pg_rowsecurity catalog.
+ */
+static void
+InsertOrUpdatePolicyRow(Relation relation, char rseccmd, Node *clause)
+{
+ Oid relationId = RelationGetRelid(relation);
+ Oid rowsecId;
+ ParseState *pstate;
+ RangeTblEntry *rte;
+ Node *qual;
+ Relation catalog;
+ ScanKeyData skeys[2];
+ SysScanDesc sscan;
+ HeapTuple oldtup;
+ HeapTuple newtup;
+ Datum values[Natts_pg_rowsecurity];
+ bool isnull[Natts_pg_rowsecurity];
+ bool replaces[Natts_pg_rowsecurity];
+ ObjectAddress target;
+ ObjectAddress myself;
+
+ /* Parse the supplied clause */
+ pstate = make_parsestate(NULL);
+
+ rte = addRangeTableEntryForRelation(pstate, relation,
+ NULL, false, false);
+ addRTEtoQuery(pstate, rte, false, true, true);
+
+ qual = transformWhereClause(pstate, copyObject(clause),
+ EXPR_KIND_ROW_SECURITY,
+ "ROW SECURITY");
+ /* zero-clear */
+ memset(values, 0, sizeof(values));
+ memset(replaces, 0, sizeof(replaces));
+ memset(isnull, 0, sizeof(isnull));
+
+ /* Update or Insert an entry to pg_rowsecurity catalog */
+ catalog = heap_open(RowSecurityRelationId, RowExclusiveLock);
+
+ ScanKeyInit(&skeys[0],
+ Anum_pg_rowsecurity_rsecrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(RelationGetRelid(relation)));
+ ScanKeyInit(&skeys[1],
+ Anum_pg_rowsecurity_rseccmd,
+ BTEqualStrategyNumber, F_CHAREQ,
+ CharGetDatum(rseccmd));
+ sscan = systable_beginscan(catalog, RowSecurityRelidIndexId, true,
+ SnapshotNow, 2, skeys);
+ oldtup = systable_getnext(sscan);
+ if (HeapTupleIsValid(oldtup))
+ {
+ rowsecId = HeapTupleGetOid(oldtup);
+
+ replaces[Anum_pg_rowsecurity_rsecqual - 1] = true;
+ values[Anum_pg_rowsecurity_rsecqual - 1]
+ = CStringGetTextDatum(nodeToString(qual));
+
+ newtup = heap_modify_tuple(oldtup,
+ RelationGetDescr(catalog),
+ values, isnull, replaces);
+ simple_heap_update(catalog, &newtup->t_self, newtup);
+
+ deleteDependencyRecordsFor(RowSecurityRelationId, rowsecId, false);
+ }
+ else
+ {
+ values[Anum_pg_rowsecurity_rsecrelid - 1]
+ = ObjectIdGetDatum(relationId);
+ values[Anum_pg_rowsecurity_rseccmd - 1]
+ = CharGetDatum(rseccmd);
+ values[Anum_pg_rowsecurity_rsecqual - 1]
+ = CStringGetTextDatum(nodeToString(qual));
+ newtup = heap_form_tuple(RelationGetDescr(catalog),
+ values, isnull);
+ rowsecId = simple_heap_insert(catalog, newtup);
+ }
+ CatalogUpdateIndexes(catalog, newtup);
+
+ heap_freetuple(newtup);
+
+ /* records dependencies of row-security policy and relation/columns */
+ target.classId = RelationRelationId;
+ target.objectId = relationId;
+ target.objectSubId = 0;
+
+ myself.classId = RowSecurityRelationId;
+ myself.objectId = rowsecId;
+ myself.objectSubId = 0;
+
+ recordDependencyOn(&myself, &target, DEPENDENCY_AUTO);
+
+ recordDependencyOnExpr(&myself, qual, pstate->p_rtable,
+ DEPENDENCY_NORMAL);
+ free_parsestate(pstate);
+
+ systable_endscan(sscan);
+ heap_close(catalog, RowExclusiveLock);
+}
+
+/*
+ * Remove row-security policy row of pg_rowsecurity
+ */
+static void
+DeletePolicyRow(Relation relation, char rseccmd)
+{
+ Assert(rseccmd == ROWSECURITY_CMD_ALL);
+
+ if (relation->rsdesc)
+ {
+ ObjectAddress address;
+
+ address.classId = RowSecurityRelationId;
+ address.objectId = relation->rsdesc->rsall.rsecid;
+ address.objectSubId = 0;
+
+ performDeletion(&address, DROP_RESTRICT, 0);
+ }
+ else
+ {
+ /* Nothing to do here */
+ elog(INFO, "relation %s has no row-security policy, skipped",
+ RelationGetRelationName(relation));
+ }
+}
+
+/*
+ * Guts of row-security policy deletion.
+ */
+void
+RemoveRowSecurityById(Oid rowsecId)
+{
+ Relation catalog;
+ ScanKeyData skey;
+ SysScanDesc sscan;
+ HeapTuple tuple;
+ Relation rel;
+ Oid relid;
+
+ catalog = heap_open(RowSecurityRelationId, RowExclusiveLock);
+
+ ScanKeyInit(&skey,
+ ObjectIdAttributeNumber,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(rowsecId));
+ sscan = systable_beginscan(catalog, RowSecurityOidIndexId, true,
+ SnapshotNow, 1, &skey);
+ tuple = systable_getnext(sscan);
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "could not find tuple for row-security %u", rowsecId);
+
+ /*
+ * Open and exclusive-lock the relation the row-security belongs to.
+ */
+ relid = ((Form_pg_rowsecurity) GETSTRUCT(tuple))->rsecrelid;
+
+ rel = heap_open(relid, AccessExclusiveLock);
+
+ simple_heap_delete(catalog, &tuple->t_self);
+
+ /* Ensure relcache entries of other session being rebuilt */
+ CacheInvalidateRelcache(rel);
+
+ heap_close(rel, NoLock);
+
+ systable_endscan(sscan);
+ heap_close(catalog, RowExclusiveLock);
+}
+
+/*
+ * ALTER TABLE <name> SET ROW SECURITY (...) OR
+ * RESET ROW SECURITY
+ */
+void
+ATExecSetRowSecurity(Relation relation, const char *cmdname, Node *clause)
+{
+ Oid relid = RelationGetRelid(relation);
+ char rseccmd;
+
+ if (strcmp(cmdname, "all") == 0)
+ rseccmd = ROWSECURITY_CMD_ALL;
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("Row-security for \"%s\" is not implemented yet",
+ cmdname)));
+
+ if (clause != NULL)
+ {
+ InsertOrUpdatePolicyRow(relation, rseccmd, clause);
+
+ /*
+ * Also, turn on relhasrowsecurity, if not.
+ */
+ if (!RelationGetForm(relation)->relhasrowsecurity)
+ {
+ Relation class_rel = heap_open(RelationRelationId,
+ RowExclusiveLock);
+ HeapTuple tuple;
+
+ tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for relation %u", relid);
+
+ ((Form_pg_class) GETSTRUCT(tuple))->relhasrowsecurity = true;
+
+ simple_heap_update(class_rel, &tuple->t_self, tuple);
+ CatalogUpdateIndexes(class_rel, tuple);
+
+ heap_freetuple(tuple);
+ heap_close(class_rel, RowExclusiveLock);
+ }
+ }
+ else
+ DeletePolicyRow(relation, rseccmd);
+
+ CacheInvalidateRelcache(relation);
+}
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 31819cc..9cb8f1b 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -24,6 +24,7 @@
#include "access/htup_details.h"
#include "access/sysattr.h"
#include "access/xact.h"
+#include "catalog/heap.h"
#include "catalog/namespace.h"
#include "catalog/pg_type.h"
#include "commands/copy.h"
@@ -34,15 +35,19 @@
#include "libpq/pqformat.h"
#include "mb/pg_wchar.h"
#include "miscadmin.h"
+#include "nodes/makefuncs.h"
#include "optimizer/clauses.h"
#include "optimizer/planner.h"
+#include "optimizer/rowsecurity.h"
#include "parser/parse_relation.h"
+#include "parser/parsetree.h"
#include "rewrite/rewriteHandler.h"
#include "storage/fd.h"
#include "tcop/tcopprot.h"
#include "utils/acl.h"
#include "utils/builtins.h"
#include "utils/lsyscache.h"
+#include "utils/syscache.h"
#include "utils/memutils.h"
#include "utils/portal.h"
#include "utils/rel.h"
@@ -814,6 +819,21 @@ DoCopy(const CopyStmt *stmt, const char *queryString, uint64 *processed)
rel = heap_openrv(stmt->relation,
(is_from ? RowExclusiveLock : AccessShareLock));
+ tupDesc = RelationGetDescr(rel);
+ attnums = CopyGetAttnums(tupDesc, rel, stmt->attlist);
+
+ /*
+ * We have to run regular query, if the target relation has
+ * row-level security policy
+ */
+ if (copy_row_security_policy((CopyStmt *)stmt, rel, attnums))
+ {
+ heap_close(rel, NoLock); /* close with keeping lock */
+ relid = InvalidOid;
+ rel = NULL;
+ }
+ else
+ {
relid = RelationGetRelid(rel);
rte = makeNode(RangeTblEntry);
@@ -822,8 +842,6 @@ DoCopy(const CopyStmt *stmt, const char *queryString, uint64 *processed)
rte->relkind = rel->rd_rel->relkind;
rte->requiredPerms = required_access;
- tupDesc = RelationGetDescr(rel);
- attnums = CopyGetAttnums(tupDesc, rel, stmt->attlist);
foreach(cur, attnums)
{
int attno = lfirst_int(cur) -
@@ -835,6 +853,7 @@ DoCopy(const CopyStmt *stmt, const char *queryString, uint64 *processed)
rte->selectedCols = bms_add_member(rte->selectedCols, attno);
}
ExecCheckRTPerms(list_make1(rte), true);
+ }
}
else
{
@@ -1193,6 +1212,53 @@ ProcessCopyOptions(CopyState cstate,
}
/*
+ * Adjust Query tree constructed with row-level security feature.
+ * If WITH OIDS option was supplied, it adds Var node to reference
+ * object-id system column.
+ */
+static void
+fixup_oid_of_rls_query(Query *query)
+{
+ RangeTblEntry *subrte;
+ TargetEntry *subtle;
+ Var *subvar;
+ ListCell *cell;
+ Form_pg_attribute attform
+ = SystemAttributeDefinition(ObjectIdAttributeNumber, true);
+
+ subrte = rt_fetch((Index) 1, query->rtable);
+ Assert(subrte->rtekind == RTE_RELATION);
+
+ if (!SearchSysCacheExists2(ATTNUM,
+ ObjectIdGetDatum(subrte->relid),
+ Int16GetDatum(attform->attnum)))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_COLUMN),
+ errmsg("table \"%s\" does not have OIDs",
+ get_rel_name(subrte->relid))));
+
+ subvar = makeVar((Index) 1,
+ attform->attnum,
+ attform->atttypid,
+ attform->atttypmod,
+ attform->attcollation,
+ 0);
+ subtle = makeTargetEntry((Expr *) subvar,
+ 0,
+ pstrdup(NameStr(attform->attname)),
+ false);
+
+ query->targetList = list_concat(list_make1(subtle),
+ query->targetList);
+ /* adjust resno of TargetEntry */
+ foreach (cell, query->targetList)
+ {
+ subtle = lfirst(cell);
+ subtle->resno++;
+ }
+}
+
+/*
* Common setup routines used by BeginCopyFrom and BeginCopyTo.
*
* Iff <binary>, unload or reload in the binary format, as opposed to the
@@ -1264,6 +1330,25 @@ BeginCopy(bool is_from,
Assert(!is_from);
cstate->rel = NULL;
+ /*
+ * In case when regular COPY TO was replaced because of row-level
+ * security, "raw_query" node have already analyzed / rewritten
+ * query tree.
+ */
+ if (IsA(raw_query, Query))
+ {
+ query = (Query *) raw_query;
+
+ Assert(query->querySource == QSRC_ROW_SECURITY);
+ if (cstate->oids)
+ {
+ fixup_oid_of_rls_query(query);
+ cstate->oids = false;
+ }
+ attnamelist = NIL;
+ }
+ else
+ {
/* Don't allow COPY w/ OIDs from a select */
if (cstate->oids)
ereport(ERROR,
@@ -1288,6 +1373,7 @@ BeginCopy(bool is_from,
elog(ERROR, "unexpected rewrite result");
query = (Query *) linitial(rewritten);
+ }
/* The grammar allows SELECT INTO, but we don't support that */
if (query->utilityStmt != NULL &&
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 328e2a8..0374629 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -992,6 +992,7 @@ EventTriggerSupportsObjectClass(ObjectClass objclass)
case OCLASS_USER_MAPPING:
case OCLASS_DEFACL:
case OCLASS_EXTENSION:
+ case OCLASS_ROWSECURITY:
return true;
case MAX_OCLASS:
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index f56ef28..770bcc2 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -37,6 +37,7 @@
#include "catalog/pg_inherits_fn.h"
#include "catalog/pg_namespace.h"
#include "catalog/pg_opclass.h"
+#include "catalog/pg_rowsecurity.h"
#include "catalog/pg_tablespace.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
@@ -2787,6 +2788,8 @@ AlterTableGetLockLevel(List *cmds)
case AT_SetTableSpace: /* must rewrite heap */
case AT_DropNotNull: /* may change some SQL plans */
case AT_SetNotNull:
+ case AT_SetRowSecurity:
+ case AT_ResetRowSecurity:
case AT_GenericOptions:
case AT_AlterColumnGenericOptions:
cmd_lockmode = AccessExclusiveLock;
@@ -3155,6 +3158,8 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
case AT_DropInherit: /* NO INHERIT */
case AT_AddOf: /* OF */
case AT_DropOf: /* NOT OF */
+ case AT_SetRowSecurity:
+ case AT_ResetRowSecurity:
ATSimplePermissions(rel, ATT_TABLE);
/* These commands never recurse */
/* No command-specific prep needed */
@@ -3440,6 +3445,12 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab, Relation rel,
case AT_DropOf:
ATExecDropOf(rel, lockmode);
break;
+ case AT_SetRowSecurity:
+ ATExecSetRowSecurity(rel, cmd->name, (Node *) cmd->def);
+ break;
+ case AT_ResetRowSecurity:
+ ATExecSetRowSecurity(rel, cmd->name, NULL);
+ break;
case AT_GenericOptions:
ATExecGenericOptions(rel, (List *) cmd->def);
break;
@@ -7788,6 +7799,22 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
Assert(defaultexpr);
break;
+ case OCLASS_ROWSECURITY:
+ /*
+ * A row-level security policy can depend on a column in case
+ * when the policy clause references a particular column.
+ * Due to same reason why TRIGGER ... WHEN does not support
+ * to change column's type being referenced in clause, row-
+ * level security policy also does not support it.
+ */
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot alter type of a column used in a row-level security policy"),
+ errdetail("%s depends on column \"%s\"",
+ getObjectDescription(&foundObject),
+ colName)));
+ break;
+
case OCLASS_PROC:
case OCLASS_TYPE:
case OCLASS_CAST:
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index b5b8d63..0835bda 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -1933,6 +1933,7 @@ _copyAppendRelInfo(const AppendRelInfo *from)
COPY_SCALAR_FIELD(parent_relid);
COPY_SCALAR_FIELD(child_relid);
+ COPY_SCALAR_FIELD(child_result);
COPY_SCALAR_FIELD(parent_reltype);
COPY_SCALAR_FIELD(child_reltype);
COPY_NODE_FIELD(translated_vars);
@@ -1974,6 +1975,7 @@ _copyRangeTblEntry(const RangeTblEntry *from)
COPY_SCALAR_FIELD(relkind);
COPY_NODE_FIELD(subquery);
COPY_SCALAR_FIELD(security_barrier);
+ COPY_SCALAR_FIELD(rowsec_relid);
COPY_SCALAR_FIELD(jointype);
COPY_NODE_FIELD(joinaliasvars);
COPY_NODE_FIELD(funcexpr);
@@ -2432,6 +2434,7 @@ _copyQuery(const Query *from)
COPY_SCALAR_FIELD(canSetTag);
COPY_NODE_FIELD(utilityStmt);
COPY_SCALAR_FIELD(resultRelation);
+ COPY_SCALAR_FIELD(sourceRelation);
COPY_SCALAR_FIELD(hasAggs);
COPY_SCALAR_FIELD(hasWindowFuncs);
COPY_SCALAR_FIELD(hasSubLinks);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 3f96595..f688275 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -805,6 +805,7 @@ _equalAppendRelInfo(const AppendRelInfo *a, const AppendRelInfo *b)
{
COMPARE_SCALAR_FIELD(parent_relid);
COMPARE_SCALAR_FIELD(child_relid);
+ COMPARE_SCALAR_FIELD(child_result);
COMPARE_SCALAR_FIELD(parent_reltype);
COMPARE_SCALAR_FIELD(child_reltype);
COMPARE_NODE_FIELD(translated_vars);
@@ -840,6 +841,7 @@ _equalQuery(const Query *a, const Query *b)
COMPARE_SCALAR_FIELD(canSetTag);
COMPARE_NODE_FIELD(utilityStmt);
COMPARE_SCALAR_FIELD(resultRelation);
+ COMPARE_SCALAR_FIELD(sourceRelation);
COMPARE_SCALAR_FIELD(hasAggs);
COMPARE_SCALAR_FIELD(hasWindowFuncs);
COMPARE_SCALAR_FIELD(hasSubLinks);
@@ -2222,6 +2224,7 @@ _equalRangeTblEntry(const RangeTblEntry *a, const RangeTblEntry *b)
COMPARE_SCALAR_FIELD(relkind);
COMPARE_NODE_FIELD(subquery);
COMPARE_SCALAR_FIELD(security_barrier);
+ COMPARE_SCALAR_FIELD(rowsec_relid);
COMPARE_SCALAR_FIELD(jointype);
COMPARE_NODE_FIELD(joinaliasvars);
COMPARE_NODE_FIELD(funcexpr);
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 42d6621..2b265dd 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -1869,8 +1869,11 @@ query_tree_walker(Query *query,
if (walker((Node *) query->targetList, context))
return true;
- if (walker((Node *) query->returningList, context))
- return true;
+ if (!(flags & QTW_IGNORE_RETURNING))
+ {
+ if (walker((Node *) query->returningList, context))
+ return true;
+ }
if (walker((Node *) query->jointree, context))
return true;
if (walker(query->setOperations, context))
@@ -2583,7 +2586,10 @@ query_tree_mutator(Query *query,
}
MUTATE(query->targetList, query->targetList, List *);
- MUTATE(query->returningList, query->returningList, List *);
+ if (!(flags & QTW_IGNORE_RETURNING))
+ MUTATE(query->returningList, query->returningList, List *);
+ else
+ query->returningList = copyObject(query->returningList);
MUTATE(query->jointree, query->jointree, FromExpr *);
MUTATE(query->setOperations, query->setOperations, Node *);
MUTATE(query->havingQual, query->havingQual, Node *);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index b2183f4..81d7774 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -1920,6 +1920,7 @@ _outAppendRelInfo(StringInfo str, const AppendRelInfo *node)
WRITE_UINT_FIELD(parent_relid);
WRITE_UINT_FIELD(child_relid);
+ WRITE_UINT_FIELD(child_result);
WRITE_OID_FIELD(parent_reltype);
WRITE_OID_FIELD(child_reltype);
WRITE_NODE_FIELD(translated_vars);
@@ -2233,6 +2234,7 @@ _outQuery(StringInfo str, const Query *node)
appendStringInfo(str, " :utilityStmt <>");
WRITE_INT_FIELD(resultRelation);
+ WRITE_INT_FIELD(sourceRelation);
WRITE_BOOL_FIELD(hasAggs);
WRITE_BOOL_FIELD(hasWindowFuncs);
WRITE_BOOL_FIELD(hasSubLinks);
@@ -2357,6 +2359,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node)
case RTE_SUBQUERY:
WRITE_NODE_FIELD(subquery);
WRITE_BOOL_FIELD(security_barrier);
+ WRITE_OID_FIELD(rowsec_relid);
break;
case RTE_JOIN:
WRITE_ENUM_FIELD(jointype, JoinType);
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 3a16e9d..1a3da23 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -199,6 +199,7 @@ _readQuery(void)
READ_BOOL_FIELD(canSetTag);
READ_NODE_FIELD(utilityStmt);
READ_INT_FIELD(resultRelation);
+ READ_INT_FIELD(sourceRelation);
READ_BOOL_FIELD(hasAggs);
READ_BOOL_FIELD(hasWindowFuncs);
READ_BOOL_FIELD(hasSubLinks);
@@ -1195,6 +1196,7 @@ _readRangeTblEntry(void)
case RTE_SUBQUERY:
READ_NODE_FIELD(subquery);
READ_BOOL_FIELD(security_barrier);
+ READ_OID_FIELD(rowsec_relid);
break;
case RTE_JOIN:
READ_ENUM_FIELD(jointype, JoinType);
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index d80c264..976b9c0 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -33,6 +33,7 @@
#include "optimizer/planmain.h"
#include "optimizer/planner.h"
#include "optimizer/prep.h"
+#include "optimizer/rowsecurity.h"
#include "optimizer/subselect.h"
#include "optimizer/tlist.h"
#include "parser/analyze.h"
@@ -176,6 +177,7 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams)
glob->lastPHId = 0;
glob->lastRowMarkId = 0;
glob->transientPlan = false;
+ glob->planUserId = InvalidOid;
/* Determine what fraction of the plan is likely to be scanned */
if (cursorOptions & CURSOR_OPT_FAST_PLAN)
@@ -253,6 +255,7 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams)
result->relationOids = glob->relationOids;
result->invalItems = glob->invalItems;
result->nParamExec = glob->nParamExec;
+ result->planUserId = glob->planUserId;
return result;
}
@@ -402,6 +405,19 @@ subquery_planner(PlannerGlobal *glob, Query *parse,
expand_inherited_tables(root);
/*
+ * Apply row-security policy of the relation being referenced,
+ * if configured with either of built-in or extension's features.
+ * RangeTblEntry of the relation with row-security policy shall
+ * be replaced with a row-security subquery that has simple scan
+ * on the target relation with row-security policy qualifiers.
+ *
+ * This routine assumes PlannerInfo is already handled with
+ * expand_inherited_tables, thus, AppendRelInfo or PlanRowMark
+ * have valid information.
+ */
+ apply_row_security_policy(root);
+
+ /*
* Set hasHavingQual to remember if HAVING clause is present. Needed
* because preprocess_expression will reduce a constant-true condition to
* an empty qual list ... but "HAVING TRUE" is not a semantic no-op.
@@ -865,6 +881,8 @@ inheritance_planner(PlannerInfo *root)
newrti = list_length(subroot.parse->rtable) + 1;
ChangeVarNodes((Node *) subroot.parse, rti, newrti, 0);
ChangeVarNodes((Node *) subroot.rowMarks, rti, newrti, 0);
+ if (subroot.parse->sourceRelation == rti)
+ subroot.parse->sourceRelation = newrti;
rte = copyObject(rte);
subroot.parse->rtable = lappend(subroot.parse->rtable,
rte);
@@ -928,7 +946,10 @@ inheritance_planner(PlannerInfo *root)
root->init_plans = subroot.init_plans;
/* Build list of target-relation RT indexes */
- resultRelations = lappend_int(resultRelations, appinfo->child_relid);
+ resultRelations = lappend_int(resultRelations,
+ (appinfo->child_result > 0 ?
+ appinfo->child_result :
+ appinfo->child_relid));
/* Build list of per-relation RETURNING targetlists */
if (parse->returningList)
diff --git a/src/backend/optimizer/prep/preptlist.c b/src/backend/optimizer/prep/preptlist.c
index fb67f9e..4abf434 100644
--- a/src/backend/optimizer/prep/preptlist.c
+++ b/src/backend/optimizer/prep/preptlist.c
@@ -37,8 +37,48 @@
static List *expand_targetlist(List *tlist, int command_type,
- Index result_relation, List *range_table);
+ 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 Var that references sub-queries being originated from regular
+ * relations with row-level security policy due to nature of sub-query
+ * that has no system-column.
+ */
+static AttrNumber
+lookup_varattno(AttrNumber attno, Index rt_index, List *rtables)
+{
+ RangeTblEntry *rte = rt_fetch(rt_index, rtables);
+
+ if (rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_SECURITY)
+ {
+ ListCell *cell;
+
+ foreach (cell, rte->subquery->targetList)
+ {
+ TargetEntry *tle = lfirst(cell);
+ Var *var;
+
+ if (IsA(tle->expr, Const))
+ continue;
+
+ var = (Var *) tle->expr;
+ Assert(IsA(var, Var));
+
+ if (var->varattno == attno)
+ return tle->resno;
+ }
+ elog(ERROR, "invalid attno %d on row-security subquery target-list",
+ attno);
+ }
+ return attno;
+}
/*
* preprocess_targetlist
@@ -73,8 +113,13 @@ preprocess_targetlist(PlannerInfo *root, List *tlist)
* 10/94
*/
if (command_type == CMD_INSERT || command_type == CMD_UPDATE)
+ {
+ Index source_relation = (parse->sourceRelation > 0 ?
+ parse->sourceRelation :
+ result_relation);
tlist = expand_targetlist(tlist, command_type,
- result_relation, range_table);
+ source_relation, range_table);
+ }
/*
* Add necessary junk columns for rowmarked rels. These values are needed
@@ -96,7 +141,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,
@@ -112,7 +158,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,
@@ -130,7 +177,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,
@@ -299,7 +346,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 e249628..0cbb035 100644
--- a/src/backend/optimizer/prep/prepunion.c
+++ b/src/backend/optimizer/prep/prepunion.c
@@ -55,6 +55,7 @@ typedef struct
{
PlannerInfo *root;
AppendRelInfo *appinfo;
+ bool in_returning;
} adjust_appendrel_attrs_context;
static Plan *recurse_set_operations(Node *setOp, PlannerInfo *root,
@@ -1594,6 +1595,7 @@ adjust_appendrel_attrs(PlannerInfo *root, Node *node, AppendRelInfo *appinfo)
context.root = root;
context.appinfo = appinfo;
+ context.in_returning = false;
/*
* Must be prepared to start with a Query or a bare expression tree.
@@ -1605,10 +1607,29 @@ adjust_appendrel_attrs(PlannerInfo *root, Node *node, AppendRelInfo *appinfo)
newnode = query_tree_mutator((Query *) node,
adjust_appendrel_attrs_mutator,
(void *) &context,
- QTW_IGNORE_RC_SUBQUERIES);
+ QTW_IGNORE_RC_SUBQUERIES |
+ QTW_IGNORE_RETURNING);
+ /*
+ * Returning clause on the relation being replaced with row-
+ * security subquery shall be handled in a special way, because
+ * of no system columns on subquery.
+ * Var references to system column or whole-row reference need
+ * to be adjusted to reference artificial columns on behalf of
+ * the underlying these columns, however, RETURNGIN clause is
+ * an exception because its Var nodes are evaluated towards
+ * the "raw" target relation, not a fetched tuple.
+ */
+ context.in_returning = true;
+ newnode->returningList = (List *)
+ expression_tree_mutator((Node *) newnode->returningList,
+ adjust_appendrel_attrs_mutator,
+ (void *) &context);
if (newnode->resultRelation == appinfo->parent_relid)
{
- newnode->resultRelation = appinfo->child_relid;
+ newnode->resultRelation = (appinfo->child_result > 0 ?
+ appinfo->child_result :
+ appinfo->child_relid);
+ newnode->sourceRelation = appinfo->child_relid;
/* Fix tlist resnos too, if it's inherited UPDATE */
if (newnode->commandType == CMD_UPDATE)
newnode->targetList =
@@ -1624,6 +1645,49 @@ adjust_appendrel_attrs(PlannerInfo *root, Node *node, AppendRelInfo *appinfo)
}
static Node *
+fixup_var_on_rowsec_subquery(RangeTblEntry *rte, Var *var)
+{
+ ListCell *cell;
+
+ Assert(rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_SECURITY);
+ /*
+ * In case when row-level security policy is applied on the referenced
+ * table, its RangeTblEntry (RTE_RELATION) is replaced with sub-query
+ * to filter out unprivileged rows of underlying relation.
+ * Even though reference to this sub-query should perform as if ones
+ * to real relations, system column has to be cared in special way
+ * due to the nature of sub-query.
+ * Target-entries that reference system columns should be added on
+ * rowlevelsec.c, so all we need to do here is looking up underlying
+ * target-list that can reference underlying system column, and fix-
+ * up varattno of the referencing Var node with resno of TargetEntry.
+ */
+ foreach (cell, rte->subquery->targetList)
+ {
+ TargetEntry *subtle = lfirst(cell);
+
+ if (IsA(subtle->expr, Var))
+ {
+ Var *subvar = (Var *) subtle->expr;
+ Var *newnode;
+
+ if (subvar->varattno == var->varattno)
+ {
+ newnode = copyObject(var);
+ newnode->varattno = subtle->resno;
+ return (Node *)newnode;
+ }
+ }
+ else
+ Assert(IsA(subtle->expr, Const));
+ }
+ elog(ERROR, "could not find artificial column of %d, relid %u",
+ var->varattno, var->varno);
+ return NULL;
+}
+
+static Node *
adjust_appendrel_attrs_mutator(Node *node,
adjust_appendrel_attrs_context *context)
{
@@ -1638,8 +1702,12 @@ adjust_appendrel_attrs_mutator(Node *node,
if (var->varlevelsup == 0 &&
var->varno == appinfo->parent_relid)
{
- var->varno = appinfo->child_relid;
+ var->varno = (context->in_returning &&
+ appinfo->child_result > 0 ?
+ appinfo->child_result :
+ appinfo->child_relid);
var->varnoold = appinfo->child_relid;
+
if (var->varattno > 0)
{
Node *newnode;
@@ -1664,6 +1732,14 @@ adjust_appendrel_attrs_mutator(Node *node,
*/
if (OidIsValid(appinfo->child_reltype))
{
+ Query *parse = context->root->parse;
+ RangeTblEntry *rte = rt_fetch(appinfo->child_relid,
+ parse->rtable);
+ if (!context->in_returning &&
+ rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_SECURITY)
+ var = (Var *)fixup_var_on_rowsec_subquery(rte, var);
+
Assert(var->vartype == appinfo->parent_reltype);
if (appinfo->parent_reltype != appinfo->child_reltype)
{
@@ -1708,7 +1784,18 @@ adjust_appendrel_attrs_mutator(Node *node,
return (Node *) rowexpr;
}
}
- /* system attributes don't need any other translation */
+ else
+ {
+ Query *parse = context->root->parse;
+ RangeTblEntry *rte;
+
+ rte = rt_fetch(appinfo->child_relid, parse->rtable);
+
+ if (!context->in_returning &&
+ rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_SECURITY)
+ return fixup_var_on_rowsec_subquery(rte, var);
+ }
}
return (Node *) var;
}
diff --git a/src/backend/optimizer/util/Makefile b/src/backend/optimizer/util/Makefile
index 3b2d16b..3f5cb19 100644
--- a/src/backend/optimizer/util/Makefile
+++ b/src/backend/optimizer/util/Makefile
@@ -13,6 +13,6 @@ top_builddir = ../../../..
include $(top_builddir)/src/Makefile.global
OBJS = clauses.o joininfo.o pathnode.o placeholder.o plancat.o predtest.o \
- relnode.o restrictinfo.o tlist.o var.o
+ relnode.o restrictinfo.o tlist.o var.o rowsecurity.o
include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/optimizer/util/rowsecurity.c b/src/backend/optimizer/util/rowsecurity.c
new file mode 100644
index 0000000..d686dfa
--- /dev/null
+++ b/src/backend/optimizer/util/rowsecurity.c
@@ -0,0 +1,733 @@
+/*
+ * optimizer/util/rowsecurity.c
+ * Routines to support row-security feature
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ */
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "access/htup_details.h"
+#include "access/sysattr.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_inherits_fn.h"
+#include "catalog/pg_rowsecurity.h"
+#include "catalog/pg_type.h"
+#include "miscadmin.h"
+#include "nodes/makefuncs.h"
+#include "nodes/nodeFuncs.h"
+#include "nodes/plannodes.h"
+#include "optimizer/clauses.h"
+#include "optimizer/prep.h"
+#include "optimizer/rowsecurity.h"
+#include "parser/parsetree.h"
+#include "rewrite/rewriteHandler.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "tcop/utility.h"
+
+/* flags to pull row-security policy */
+#define RSEC_FLAG_HAS_SUBLINKS 0x0001
+
+/* hook to allow extensions to apply their own security policy */
+row_security_policy_hook_type row_security_policy_hook = NULL;
+
+/*
+ * make_artificial_column
+ *
+ * It makes a target-entry node that references underlying column.
+ * Its tle->expr is usualy Var node, but may be Const for dummy NULL
+ * if the supplied attribute was already dropped.
+ */
+static TargetEntry *
+make_artificial_column(RangeTblEntry *subrte, AttrNumber attnum)
+{
+ Expr *expr;
+ char *resname;
+
+ Assert(subrte->rtekind == RTE_RELATION && OidIsValid(subrte->relid));
+ if (attnum == InvalidAttrNumber)
+ {
+ expr = (Expr *) makeWholeRowVar(subrte, (Index) 1, 0, false);
+ resname = get_rel_name(subrte->relid);
+ }
+ else
+ {
+ HeapTuple tuple;
+ Form_pg_attribute attform;
+
+ tuple = SearchSysCache2(ATTNUM,
+ ObjectIdGetDatum(subrte->relid),
+ Int16GetDatum(attnum));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for attribute %d of relation %u",
+ attnum, subrte->relid);
+ attform = (Form_pg_attribute) GETSTRUCT(tuple);
+
+ if (attform->attisdropped)
+ {
+ char namebuf[NAMEDATALEN];
+
+ /* Insert NULL just for a placeholder of dropped column */
+ expr = (Expr *) makeConst(INT4OID,
+ -1,
+ InvalidOid,
+ sizeof(int32),
+ (Datum) 0,
+ true, /* isnull */
+ true); /* byval */
+ sprintf(namebuf, "dummy-%d", (int)attform->attnum);
+ resname = pstrdup(namebuf);
+ }
+ else
+ {
+ expr = (Expr *) makeVar((Index) 1,
+ attform->attnum,
+ attform->atttypid,
+ attform->atttypmod,
+ attform->attcollation,
+ 0);
+ resname = pstrdup(NameStr(attform->attname));
+ }
+ ReleaseSysCache(tuple);
+ }
+ return makeTargetEntry(expr, -1, resname, false);
+}
+
+/*
+ * lookup_artificial_column
+ *
+ * It looks-up resource number of the target-entry relevant to the given
+ * Var-node that references the row-security subquery. If required column
+ * is not in the subquery's target-list, this function also adds new one
+ * and returns its resource number.
+ */
+static AttrNumber
+lookup_artificial_column(PlannerInfo *root,
+ RangeTblEntry *rte, AttrNumber varattno)
+{
+ Query *subqry;
+ RangeTblEntry *subrte;
+ TargetEntry *subtle;
+ ListCell *cell;
+
+ Assert(rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_SECURITY);
+
+ subqry = rte->subquery;
+ foreach (cell, subqry->targetList)
+ {
+ subtle = lfirst(cell);
+
+ /*
+ * If referenced artifical column is already constructed on the
+ * target-list of row-security subquery, nothing to do any more.
+ */
+ if (IsA(subtle->expr, Var))
+ {
+ Var *subvar = (Var *)subtle->expr;
+
+ Assert(subvar->varno == 1);
+ if (subvar->varattno == varattno)
+ return subtle->resno;
+ }
+ }
+
+ /*
+ * OK, we don't have an artifical column relevant to the required ones,
+ * so let's create a new artifical column on demand.
+ */
+ subrte = rt_fetch((Index) 1, subqry->rtable);
+ subtle = make_artificial_column(subrte, varattno);
+ subtle->resno = list_length(subqry->targetList) + 1;
+
+ subqry->targetList = lappend(subqry->targetList, subtle);
+ rte->eref->colnames = lappend(rte->eref->colnames,
+ makeString(pstrdup(subtle->resname)));
+ return subtle->resno;
+}
+
+/*
+ * fixup_varnode_walker
+ *
+ * It recursively fixes up references to the relation to be replaced by
+ * row-security sub-query, and adds artificial columns relevant to the
+ * underlying system columns or whole row-reference on demand.
+ */
+typedef struct {
+ PlannerInfo *root;
+ int varlevelsup;
+ Index *vartrans;
+} fixup_varnode_context;
+
+static bool
+fixup_varnode_walker(Node *node, fixup_varnode_context *context)
+{
+ if (node == NULL)
+ return false;
+
+ if (IsA(node, Var))
+ {
+ Var *var = (Var *) node;
+ List *rtable = context->root->parse->rtable;
+ RangeTblEntry *rte;
+ ListCell *cell;
+
+ /*
+ * Ignore it, if Var node does not reference the Query currently
+ * we focues on.
+ */
+ if (var->varlevelsup != context->varlevelsup)
+ return false;
+
+ /*
+ * Var nodes that reference the relation being replaced by row-
+ * security sub-query has to be adjusted; to reference the sub-
+ * query, instead of the original relation.
+ */
+ if (context->vartrans[var->varno] != 0)
+ {
+ rte = rt_fetch(context->vartrans[var->varno], rtable);
+ if (rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_SECURITY)
+ {
+ var->varno = var->varnoold = context->vartrans[var->varno];
+ var->varattno = lookup_artificial_column(context->root,
+ rte, var->varattno);
+ }
+ }
+ else
+ {
+ rte = rt_fetch(var->varno, rtable);
+ if (!rte->inh)
+ return false;
+
+ foreach (cell, context->root->append_rel_list)
+ {
+ AppendRelInfo *appinfo = lfirst(cell);
+ RangeTblEntry *child_rte;
+
+ if (appinfo->parent_relid != var->varno)
+ continue;
+
+ if (var->varattno > InvalidAttrNumber)
+ continue;
+
+ child_rte = rt_fetch(appinfo->child_relid, rtable);
+ if (child_rte->rtekind == RTE_SUBQUERY &&
+ child_rte->subquery->querySource == QSRC_ROW_SECURITY)
+ (void) lookup_artificial_column(context->root,
+ child_rte,
+ var->varattno);
+ }
+ }
+ }
+ else if (IsA(node, RangeTblRef))
+ {
+ RangeTblRef *rtr = (RangeTblRef *) node;
+
+ if (context->varlevelsup == 0 &&
+ context->vartrans[rtr->rtindex] != 0)
+ rtr->rtindex = context->vartrans[rtr->rtindex];
+ }
+ else if (IsA(node, Query))
+ {
+ bool result;
+
+ context->varlevelsup++;
+ result = query_tree_walker((Query *) node,
+ fixup_varnode_walker,
+ (void *) context, 0);
+ context->varlevelsup--;
+
+ return result;
+ }
+ return expression_tree_walker(node,
+ fixup_varnode_walker,
+ (void *) context);
+}
+
+/*
+ * check_infinite_recursion
+ *
+ * It is a wrong row-security configuration, if we try to expand
+ * the relation inside of row-security subquery originated from
+ * same relation!
+ */
+static void
+check_infinite_recursion(PlannerInfo *root, Oid relid)
+{
+ PlannerInfo *parent = root->parent_root;
+
+ if (parent && parent->parse->querySource == QSRC_ROW_SECURITY)
+ {
+ RangeTblEntry *rte = rt_fetch(1, parent->parse->rtable);
+
+ Assert(rte->rtekind == RTE_RELATION && OidIsValid(rte->relid));
+
+ if (relid == rte->relid)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("infinite recursion detected for relation \"%s\"",
+ get_rel_name(relid))));
+ check_infinite_recursion(parent, relid);
+ }
+}
+
+/*
+ * expand_rtentry_with_policy
+ *
+ * It extends a range-table entry of row-security sub-query with supplied
+ * security policy, and append it on the parse->rtable.
+ * This sub-query contains artificial columns that reference underlying
+ * regular columns (at least, references to system column or whole of
+ * table reference shall be added on demand), and simple scan on the
+ * target relation.
+ * Any Var nodes that referenced the relation pointed by rtindex shall
+ * be adjusted to reference this sub-query instead. walker
+ *
+ */
+static Index
+expand_rtentry_with_policy(PlannerInfo *root, Index rtindex,
+ Expr *qual, int flags)
+{
+ Query *parse = root->parse;
+ RangeTblEntry *rte = rt_fetch(rtindex, parse->rtable);
+ Query *subqry;
+ RangeTblEntry *subrte;
+ RangeTblRef *subrtr;
+ TargetEntry *subtle;
+ RangeTblEntry *newrte;
+ HeapTuple tuple;
+ AttrNumber nattrs;
+ AttrNumber attnum;
+ List *targetList = NIL;
+ List *colNameList = NIL;
+ PlanRowMark *rowmark;
+
+ Assert(rte->rtekind == RTE_RELATION && !rte->inh);
+
+ /* check recursion to prevent infinite loop */
+ check_infinite_recursion(root, rte->relid);
+
+ /* Expand views inside SubLink node */
+ if (flags & RSEC_FLAG_HAS_SUBLINKS)
+ QueryRewriteExpr((Node *)qual, list_make1_oid(rte->relid));
+
+ /*
+ * Construction of sub-query
+ */
+ subqry = (Query *) makeNode(Query);
+ subqry->commandType = CMD_SELECT;
+ subqry->querySource = QSRC_ROW_SECURITY;
+
+ subrte = copyObject(rte);
+ subqry->rtable = list_make1(subrte);
+
+ subrtr = makeNode(RangeTblRef);
+ subrtr->rtindex = 1;
+ subqry->jointree = makeFromExpr(list_make1(subrtr), (Node *) qual);
+ if (flags & RSEC_FLAG_HAS_SUBLINKS)
+ subqry->hasSubLinks = true;
+
+ /*
+ * Construction of TargetEntries that reference underlying columns.
+ */
+ tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(rte->relid));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for relation %u", rte->relid);
+ nattrs = ((Form_pg_class) GETSTRUCT(tuple))->relnatts;
+ ReleaseSysCache(tuple);
+
+ for (attnum = 1; attnum <= nattrs; attnum++)
+ {
+ subtle = make_artificial_column(subrte, attnum);
+ subtle->resno = list_length(targetList) + 1;
+ Assert(subtle->resno == attnum);
+
+ targetList = lappend(targetList, subtle);
+ colNameList = lappend(colNameList,
+ makeString(pstrdup(subtle->resname)));
+ }
+ subqry->targetList = targetList;
+
+ /* Expand RengeTblEntry with this sub-query */
+ newrte = makeNode(RangeTblEntry);
+ newrte->rtekind = RTE_SUBQUERY;
+ newrte->subquery = subqry;
+ newrte->security_barrier = true;
+ newrte->rowsec_relid = rte->relid;
+ newrte->eref = makeAlias(get_rel_name(rte->relid), colNameList);
+
+ parse->rtable = lappend(parse->rtable, newrte);
+
+ /* Push-down rowmark, if needed */
+ rowmark = get_plan_rowmark(root->rowMarks, rtindex);
+ if (rowmark)
+ {
+ /*
+ * In case of inherited children, rti/prti of rowmark shall be
+ * fixed up later, on inheritance_planner().
+ */
+ if (rowmark->rti == rowmark->prti)
+ rowmark->rti = rowmark->prti = list_length(parse->rtable);
+ else
+ rowmark->rti = list_length(parse->rtable);
+
+ lookup_artificial_column(root, newrte, SelfItemPointerAttributeNumber);
+ lookup_artificial_column(root, newrte, TableOidAttributeNumber);
+ }
+
+ return list_length(parse->rtable);
+}
+
+/*
+ * pull_row_security_policy
+ *
+ * It pulls the configured row-security policy of both built-in and
+ * extensions. If any, it returns expression tree.
+ */
+static Expr *
+pull_row_security_policy(CmdType cmd, Relation relation, int *p_flags)
+{
+ Expr *quals = NULL;
+ int flags = 0;
+
+ /*
+ * Pull the row-security policy configured with built-in features,
+ * if unprivileged users. Please note that superuser can bypass it.
+ */
+ if (relation->rsdesc && !superuser())
+ {
+ RowSecurityDesc *rsdesc = relation->rsdesc;
+
+ quals = copyObject(rsdesc->rsall.qual);
+ if (rsdesc->rsall.hassublinks)
+ flags |= RSEC_FLAG_HAS_SUBLINKS;
+ }
+
+ /*
+ * Also, ask extensions whether they want to apply their own
+ * row-security policy. If both built-in and extension has
+ * their own policy, it shall be merged.
+ */
+ if (row_security_policy_hook)
+ {
+ List *temp;
+
+ temp = (*row_security_policy_hook)(cmd, relation);
+ if (temp != NIL)
+ {
+ if ((flags & RSEC_FLAG_HAS_SUBLINKS) == 0 &&
+ contain_subplans((Node *) temp))
+ flags |= RSEC_FLAG_HAS_SUBLINKS;
+
+ if (quals != NULL)
+ temp = lappend(temp, quals);
+
+ if (list_length(temp) == 1)
+ quals = (Expr *)list_head(temp);
+ else if (list_length(temp) > 1)
+ quals = makeBoolExpr(AND_EXPR, temp, -1);
+ }
+ }
+ *p_flags = flags;
+ return quals;
+}
+
+/*
+ * copy_row_security_policy
+ *
+ * It construct a row-security subquery instead of raw COPY TO statement,
+ * if target relation has a row-level security policy
+ */
+bool
+copy_row_security_policy(CopyStmt *stmt, Relation rel, List *attnums)
+{
+ Expr *quals;
+ int flags;
+ Query *parse;
+ RangeTblEntry *rte;
+ RangeTblRef *rtr;
+ TargetEntry *tle;
+ Var *var;
+ ListCell *cell;
+
+ if (stmt->is_from)
+ return false;
+
+ quals = pull_row_security_policy(CMD_SELECT, rel, &flags);
+ if (!quals)
+ return false;
+
+ parse = (Query *) makeNode(Query);
+ parse->commandType = CMD_SELECT;
+ parse->querySource = QSRC_ROW_SECURITY;
+
+ rte = makeNode(RangeTblEntry);
+ rte->rtekind = RTE_RELATION;
+ rte->relid = RelationGetRelid(rel);
+ rte->relkind = RelationGetForm(rel)->relkind;
+
+ foreach (cell, attnums)
+ {
+ HeapTuple tuple;
+ Form_pg_attribute attform;
+ AttrNumber attno = lfirst_int(cell);
+
+ tuple = SearchSysCache2(ATTNUM,
+ ObjectIdGetDatum(RelationGetRelid(rel)),
+ Int16GetDatum(attno));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for attribute %d of relation %s",
+ attno, RelationGetRelationName(rel));
+ attform = (Form_pg_attribute) GETSTRUCT(tuple);
+
+ var = makeVar((Index) 1,
+ attform->attnum,
+ attform->atttypid,
+ attform->atttypmod,
+ attform->attcollation,
+ 0);
+ tle = makeTargetEntry((Expr *) var,
+ list_length(parse->targetList) + 1,
+ pstrdup(NameStr(attform->attname)),
+ false);
+ parse->targetList = lappend(parse->targetList, tle);
+
+ ReleaseSysCache(tuple);
+
+ rte->selectedCols = bms_add_member(rte->selectedCols,
+ attno - FirstLowInvalidHeapAttributeNumber);
+ }
+ rte->inFromCl = true;
+ rte->requiredPerms = ACL_SELECT;
+
+ rtr = makeNode(RangeTblRef);
+ rtr->rtindex = 1;
+
+ parse->jointree = makeFromExpr(list_make1(rtr), (Node *) quals);
+ parse->rtable = list_make1(rte);
+ if (flags & RSEC_FLAG_HAS_SUBLINKS)
+ parse->hasSubLinks = true;
+
+ stmt->query = (Node *) parse;
+
+ return true;
+}
+
+/*
+ * apply_row_security_relation
+ *
+ * It applies row-security policy on a particular relation being specified.
+ * If this relation is top of the inheritance tree, it also checks inherited
+ * children.
+ */
+static bool
+apply_row_security_relation(PlannerInfo *root, Index *vartrans,
+ CmdType cmd, Index rtindex)
+{
+ Query *parse = root->parse;
+ RangeTblEntry *rte = rt_fetch(rtindex, parse->rtable);
+ bool result = false;
+
+ if (!rte->inh)
+ {
+ Relation rel;
+ Expr *qual;
+ int flags = 0;
+
+ rel = heap_open(rte->relid, NoLock);
+ qual = pull_row_security_policy(cmd, rel, &flags);
+ if (qual)
+ {
+ vartrans[rtindex]
+ = expand_rtentry_with_policy(root, rtindex, qual, flags);
+ if (parse->resultRelation == rtindex)
+ parse->sourceRelation = vartrans[rtindex];
+ result = true;
+ }
+ heap_close(rel, NoLock);
+ }
+ else
+ {
+ ListCell *lc1, *lc2;
+
+ foreach (lc1, root->append_rel_list)
+ {
+ AppendRelInfo *apinfo = lfirst(lc1);
+
+ if (apinfo->parent_relid != rtindex)
+ continue;
+
+ if (apply_row_security_relation(root, vartrans, cmd,
+ apinfo->child_relid))
+ {
+ if (parse->resultRelation == rtindex)
+ apinfo->child_result = apinfo->child_relid;
+ apinfo->child_relid = vartrans[apinfo->child_relid];
+ foreach (lc2, apinfo->translated_vars)
+ {
+ Var *var = lfirst(lc2);
+
+ if (var)
+ var->varno = apinfo->child_relid;
+ }
+ result = true;
+ }
+ }
+ }
+ return result;
+}
+
+/*
+ * apply_row_security_recursive
+ *
+ * walker on join-tree
+ */
+static bool
+apply_row_security_recursive(PlannerInfo *root, Index *vartrans, Node *jtnode)
+{
+ bool result = false;
+
+ if (jtnode == NULL)
+ return false;
+ if (IsA(jtnode, RangeTblRef))
+ {
+ Index rtindex = ((RangeTblRef *) jtnode)->rtindex;
+ Query *parse = root->parse;
+ RangeTblEntry *rte = rt_fetch(rtindex, parse->rtable);
+ CmdType cmd;
+
+ /* Only relation can have row-security policy */
+ if (rte->rtekind != RTE_RELATION)
+ return false;
+
+ /*
+ * Prevents infinite recursion. Please note that rtindex == 1
+ * of the row-security subquery is a relation being already
+ * processed on the upper level.
+ */
+ if (parse->querySource == QSRC_ROW_SECURITY && rtindex == 1)
+ return false;
+
+ /* Is it a result relation of UPDATE or DELETE command? */
+ if (parse->resultRelation == rtindex)
+ cmd = parse->commandType;
+ else
+ cmd = CMD_SELECT;
+
+ /* Try to apply row-security policy, if configured */
+ result = apply_row_security_relation(root, vartrans, cmd, rtindex);
+ }
+ else if (IsA(jtnode, FromExpr))
+ {
+ FromExpr *f = (FromExpr *) jtnode;
+ ListCell *l;
+
+ foreach (l, f->fromlist)
+ {
+ if (apply_row_security_recursive(root, vartrans, lfirst(l)))
+ result = true;
+ }
+ }
+ else if (IsA(jtnode, JoinExpr))
+ {
+ JoinExpr *j = (JoinExpr *) jtnode;
+
+ if (apply_row_security_recursive(root, vartrans, j->larg))
+ result = true;
+ if (apply_row_security_recursive(root, vartrans, j->rarg))
+ result = true;
+ }
+ else
+ elog(ERROR, "unexpected node type: %d", (int) nodeTag(jtnode));
+
+ return result;
+}
+
+/*
+ * apply_row_security_policy
+ *
+ * Entrypoint to apply configured row-security policy of the relation.
+ *
+ * In case when the supplied query references relations with row-security
+ * policy, its RangeTblEntry shall be replaced by a row-security subquery
+ * that has simple scan on the referenced table with policy qualifiers.
+ * Of course, security-barrier shall be set on the subquery to prevent
+ * unexpected push-down of functions without leakproof flag.
+ *
+ * For example, when table t1 has a security policy "(x % 2 = 0)", the
+ * following query:
+ * SELECT * FROM t1 WHERE f_leak(y)
+ * performs as if
+ * SELECT * FROM (
+ * SELECT x, y FROM t1 WHERE (x % 2 = 0)
+ * ) AS t1 WHERE f_leak(y)
+ * would be given. Because the sub-query has security barrier flag,
+ * configured security policy qualifier is always executed prior to
+ * user given functions.
+ */
+void
+apply_row_security_policy(PlannerInfo *root)
+{
+ Query *parse = root->parse;
+ Oid curr_userid;
+ int curr_seccxt;
+ Index *vartrans;
+
+ /*
+ * Mode checks. In case when SECURITY_ROW_LEVEL_DISABLED is set,
+ * no row-level security policy should be applied regardless
+ * whether it is built-in or extension.
+ */
+ GetUserIdAndSecContext(&curr_userid, &curr_seccxt);
+ if (curr_seccxt & SECURITY_ROW_LEVEL_DISABLED)
+ return;
+
+ vartrans = palloc0(sizeof(Index) * (list_length(parse->rtable) + 1));
+ if (apply_row_security_recursive(root, vartrans, (Node *)parse->jointree))
+ {
+ PlannerGlobal *glob = root->glob;
+ PlanInvalItem *pi_item;
+ fixup_varnode_context context;
+
+ /*
+ * Constructed Plan with row-level security policy depends on
+ * properties of current user (database superuser can bypass
+ * configured row-security policy!), thus, it has to be
+ * invalidated when its assumption was changed.
+ */
+ if (!OidIsValid(glob->planUserId))
+ {
+ /* Plan invalidation on session user-id */
+ glob->planUserId = GetUserId();
+
+ /* Plan invalidation on catalog updates of pg_authid */
+ pi_item = makeNode(PlanInvalItem);
+ pi_item->cacheId = AUTHOID;
+ pi_item->hashValue =
+ GetSysCacheHashValue1(AUTHOID,
+ ObjectIdGetDatum(glob->planUserId));
+ glob->invalItems = lappend(glob->invalItems, pi_item);
+ }
+ else
+ Assert(glob->planUserId == GetUserId());
+
+ /*
+ * Var-nodes that referenced RangeTblEntry to be replaced by
+ * row-security sub-query have to be adjusted for appropriate
+ * reference to the underlying artificial column of the relation.
+ */
+ context.root = root;
+ context.varlevelsup = 0;
+ context.vartrans = vartrans;
+ query_tree_walker(parse,
+ fixup_varnode_walker,
+ (void *) &context,
+ QTW_IGNORE_RETURNING);
+ }
+ pfree(vartrans);
+}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index f67ef0c..af2ae94 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -256,6 +256,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
%type <node> alter_table_cmd alter_type_cmd opt_collate_clause
%type <list> alter_table_cmds alter_type_cmds
+%type <str> row_security_cmd
%type <dbehavior> opt_drop_behavior
@@ -2173,6 +2174,24 @@ alter_table_cmd:
n->def = (Node *)$2;
$$ = (Node *)n;
}
+ /* ALTER TABLE <name> SET ROW SECURITY FOR <cmd> TO (<expr>) */
+ | SET ROW SECURITY FOR row_security_cmd TO '(' a_expr ')'
+ {
+ AlterTableCmd *n = makeNode(AlterTableCmd);
+ n->subtype = AT_SetRowSecurity;
+ n->name = $5;
+ n->def = (Node *) $8;
+ $$ = (Node *)n;
+ }
+ /* ALTER TABLE <name> RESET ROW SECURITY FOR <cmd> */
+ | RESET ROW SECURITY FOR row_security_cmd
+ {
+ AlterTableCmd *n = makeNode(AlterTableCmd);
+ n->subtype = AT_ResetRowSecurity;
+ n->name = $5;
+ n->def = NULL;
+ $$ = (Node *)n;
+ }
| alter_generic_options
{
AlterTableCmd *n = makeNode(AlterTableCmd);
@@ -2244,6 +2263,12 @@ reloption_elem:
}
;
+row_security_cmd: ALL { $$ = "all"; }
+ | SELECT { $$ = "select"; }
+ | INSERT { $$ = "insert"; }
+ | UPDATE { $$ = "update"; }
+ | DELETE_P { $$ = "delete"; }
+ ;
/*****************************************************************************
*
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
index 7380618..14e02d4 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -269,6 +269,9 @@ transformAggregateCall(ParseState *pstate, Aggref *agg,
case EXPR_KIND_TRIGGER_WHEN:
err = _("aggregate functions are not allowed in trigger WHEN conditions");
break;
+ case EXPR_KIND_ROW_SECURITY:
+ err = _("aggregate functions are not allowed in row-security policy");
+ break;
/*
* There is intentionally no default: case here, so that the
@@ -537,6 +540,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
case EXPR_KIND_TRIGGER_WHEN:
err = _("window functions are not allowed in trigger WHEN conditions");
break;
+ case EXPR_KIND_ROW_SECURITY:
+ err = _("window functions are not allowed in row-security policy");
+ break;
/*
* There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 06f6512..0334ecb 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -1445,6 +1445,7 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
case EXPR_KIND_OFFSET:
case EXPR_KIND_RETURNING:
case EXPR_KIND_VALUES:
+ case EXPR_KIND_ROW_SECURITY:
/* okay */
break;
case EXPR_KIND_CHECK_CONSTRAINT:
@@ -2624,6 +2625,8 @@ ParseExprKindName(ParseExprKind exprKind)
return "EXECUTE";
case EXPR_KIND_TRIGGER_WHEN:
return "WHEN";
+ case EXPR_KIND_ROW_SECURITY:
+ return "ROW SECURITY";
/*
* There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c
index a9254c8..768d531 100644
--- a/src/backend/parser/parse_relation.c
+++ b/src/backend/parser/parse_relation.c
@@ -2058,6 +2058,25 @@ expandRelAttrs(ParseState *pstate, RangeTblEntry *rte,
}
/*
+ * getrelid
+ *
+ * Get OID of the relation corresponding to the given range index.
+ * Note that InvalidOid will be returned if the RTE is for neither
+ * relation nor sub-query originated from a relation
+ */
+Oid
+getrelid(Index rangeindex, List *rangetable)
+{
+ RangeTblEntry *rte = rt_fetch(rangeindex, rangetable);
+
+ if (rte->rtekind == RTE_RELATION)
+ return rte->relid;
+ if (rte->rtekind == RTE_SUBQUERY)
+ return rte->rowsec_relid;
+ return InvalidOid;
+}
+
+/*
* get_rte_attribute_name
* Get an attribute name from a RangeTblEntry
*
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index d909de3..1c8c984 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -3002,3 +3002,19 @@ QueryRewrite(Query *parsetree)
return results;
}
+
+/*
+ * QueryRewriteExpr
+ *
+ * This routine provides an entry point of query rewriter towards
+ * a certain expression tree with SubLink node; being added after
+ * the top level query rewrite.
+ * It primarily intends to expand views appeared in the qualifiers
+ * appended with row-level security which needs to modify query
+ * tree at head of the planner stage.
+ */
+void
+QueryRewriteExpr(Node *node, List *activeRIRs)
+{
+ fireRIRonSubLink(node, activeRIRs);
+}
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index 65edc1f..b36da97 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -3008,6 +3008,7 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
int spi_result;
Oid save_userid;
int save_sec_context;
+ int temp_sec_context;
Datum vals[RI_MAX_NUMKEYS * 2];
char nulls[RI_MAX_NUMKEYS * 2];
@@ -3087,8 +3088,18 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
/* Switch to proper UID to perform check as */
GetUserIdAndSecContext(&save_userid, &save_sec_context);
+
+ /*
+ * Row-level security should be disabled in case when foreign-key
+ * relation is queried to check existence of tuples that references
+ * the primary-key being modified.
+ */
+ temp_sec_context = save_sec_context | SECURITY_LOCAL_USERID_CHANGE;
+ if (source_is_pk)
+ temp_sec_context |= SECURITY_ROW_LEVEL_DISABLED;
+
SetUserIdAndSecContext(RelationGetForm(query_rel)->relowner,
- save_sec_context | SECURITY_LOCAL_USERID_CHANGE);
+ temp_sec_context);
/* Finally we can run the query. */
spi_result = SPI_execute_snapshot(qplan,
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index 26cae97..d05cde7 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -53,6 +53,7 @@
#include "catalog/namespace.h"
#include "executor/executor.h"
#include "executor/spi.h"
+#include "miscadmin.h"
#include "nodes/nodeFuncs.h"
#include "optimizer/planmain.h"
#include "optimizer/prep.h"
@@ -794,6 +795,16 @@ CheckCachedPlan(CachedPlanSource *plansource)
AcquireExecutorLocks(plan->stmt_list, true);
/*
+ * If plan was constructed with assumption of a particular user-id,
+ * and it is different from the current one, the cached-plan shall
+ * be invalidated to construct suitable query plan.
+ */
+ if (plan->is_valid &&
+ OidIsValid(plan->planUserId) &&
+ plan->planUserId == GetUserId())
+ plan->is_valid = false;
+
+ /*
* If plan was transient, check to see if TransactionXmin has
* advanced, and if so invalidate it.
*/
@@ -846,6 +857,8 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
{
CachedPlan *plan;
List *plist;
+ ListCell *cell;
+ Oid planUserId = InvalidOid;
bool snapshot_set;
bool spi_pushed;
MemoryContext plan_context;
@@ -913,6 +926,24 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
PopActiveSnapshot();
/*
+ * Check whether the generated plan assumes a particular user-id, or not.
+ * In case when a valid user-id is recorded on PlannedStmt->planUserId,
+ * it should be kept and used to validation check of the cached plan
+ * under the "current" user-id.
+ */
+ foreach (cell, plist)
+ {
+ PlannedStmt *pstmt = lfirst(cell);
+
+ if (IsA(pstmt, PlannedStmt) && OidIsValid(pstmt->planUserId))
+ {
+ Assert(!OidIsValid(planUserId) || planUserId == pstmt->planUserId);
+
+ planUserId = pstmt->planUserId;
+ }
+ }
+
+ /*
* Normally we make a dedicated memory context for the CachedPlan and its
* subsidiary data. (It's probably not going to be large, but just in
* case, use the default maxsize parameter. It's transient for the
@@ -955,6 +986,7 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
plan->is_oneshot = plansource->is_oneshot;
plan->is_saved = false;
plan->is_valid = true;
+ plan->planUserId = planUserId;
/* assign generation number to new plan */
plan->generation = ++(plansource->generation);
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 66fb63b..828c10e 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -50,6 +50,7 @@
#include "catalog/pg_opclass.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_rewrite.h"
+#include "catalog/pg_rowsecurity.h"
#include "catalog/pg_tablespace.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
@@ -935,6 +936,11 @@ RelationBuildDesc(Oid targetRelId, bool insertIt)
else
relation->trigdesc = NULL;
+ if (relation->rd_rel->relhasrowsecurity)
+ RelationBuildRowSecurity(relation);
+ else
+ relation->rsdesc = NULL;
+
/*
* if it's an index, initialize index-related information
*/
@@ -1842,6 +1848,8 @@ RelationDestroyRelation(Relation relation)
MemoryContextDelete(relation->rd_indexcxt);
if (relation->rd_rulescxt)
MemoryContextDelete(relation->rd_rulescxt);
+ if (relation->rsdesc)
+ MemoryContextDelete(relation->rsdesc->rscxt);
if (relation->rd_fdwroutine)
pfree(relation->rd_fdwroutine);
pfree(relation);
@@ -3161,7 +3169,13 @@ RelationCacheInitializePhase3(void)
relation->rd_rel->relhastriggers = false;
restart = true;
}
-
+ if (relation->rd_rel->relhasrowsecurity && relation->rsdesc == NULL)
+ {
+ RelationBuildRowSecurity(relation);
+ if (relation->rsdesc == NULL)
+ relation->rd_rel->relhasrowsecurity = false;
+ restart = true;
+ }
/* Release hold on the relation */
RelationDecrementReferenceCount(relation);
@@ -4407,6 +4421,7 @@ load_relcache_init_file(bool shared)
rel->rd_rules = NULL;
rel->rd_rulescxt = NULL;
rel->trigdesc = NULL;
+ rel->rsdesc = NULL;
rel->rd_indexprs = NIL;
rel->rd_indpred = NIL;
rel->rd_exclops = NULL;
diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c
index 247ad92..9603ef4 100644
--- a/src/bin/pg_dump/common.c
+++ b/src/bin/pg_dump/common.c
@@ -244,6 +244,10 @@ getSchemaData(Archive *fout, int *numTablesPtr)
write_msg(NULL, "reading rewrite rules\n");
getRules(fout, &numRules);
+ if (g_verbose)
+ write_msg(NULL, "reading row-security policies\n");
+ getRowSecurity(fout, tblinfo, numTables);
+
*numTablesPtr = numTables;
return tblinfo;
}
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index cd7669b..e9ca251 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3148,6 +3148,7 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, RestoreOptions *ropt, bool isDat
strcmp(te->desc, "INDEX") == 0 ||
strcmp(te->desc, "RULE") == 0 ||
strcmp(te->desc, "TRIGGER") == 0 ||
+ strcmp(te->desc, "ROW SECURITY") == 0 ||
strcmp(te->desc, "USER MAPPING") == 0)
{
/* these object types don't have separate owners */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index f40961f..152ca03 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -249,6 +249,7 @@ static char *myFormatType(const char *typname, int32 typmod);
static void getBlobs(Archive *fout);
static void dumpBlob(Archive *fout, BlobInfo *binfo);
static int dumpBlobs(Archive *fout, void *arg);
+static void dumpRowSecurity(Archive *fout, RowSecurityInfo *rsinfo);
static void dumpDatabase(Archive *AH);
static void dumpEncoding(Archive *AH);
static void dumpStdStrings(Archive *AH);
@@ -2686,6 +2687,134 @@ dumpBlobs(Archive *fout, void *arg)
return 1;
}
+/*
+ * getRowSecurity
+ * get information about every row-security policy on a dumpable table
+ */
+void
+getRowSecurity(Archive *fout, TableInfo tblinfo[], int numTables)
+{
+ PQExpBuffer query = createPQExpBuffer();
+ PGresult *res;
+ RowSecurityInfo *rsinfo;
+ int i_oid;
+ int i_tableoid;
+ int i_rseccmd;
+ int i_rsecqual;
+ int i, j, ntups;
+
+ /* row-security is not supported prior to v9.4 */
+ if (fout->remoteVersion < 90400)
+ return;
+
+ for (i=0; i < numTables; i++)
+ {
+ TableInfo *tbinfo = &tblinfo[i];
+
+ if (!tbinfo->hasrowsec || !tbinfo->dobj.dump)
+ continue;
+
+ if (g_verbose)
+ write_msg(NULL, "reading row-security policy for table \"%s\"\n",
+ tbinfo->dobj.name);
+
+ /*
+ * select table schema to ensure regproc name is qualified if needed
+ */
+ selectSourceSchema(fout, tbinfo->dobj.namespace->dobj.name);
+
+ resetPQExpBuffer(query);
+
+ appendPQExpBuffer(query,
+ "SELECT oid, tableoid, s.rseccmd, "
+ "pg_get_expr(s.rsecqual, s.rsecrelid) AS rsecqual "
+ "FROM pg_catalog.pg_rowsecurity s "
+ "WHERE rsecrelid = '%u'",
+ tbinfo->dobj.catId.oid);
+ res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+
+ ntups = PQntuples(res);
+
+ i_oid = PQfnumber(res, "oid");
+ i_tableoid = PQfnumber(res, "tableoid");
+ i_rseccmd = PQfnumber(res, "rseccmd");
+ i_rsecqual = PQfnumber(res, "rsecqual");
+
+ rsinfo = pg_malloc(ntups * sizeof(RowSecurityInfo));
+ for (j=0; j < ntups; j++)
+ {
+ char namebuf[NAMEDATALEN + 1];
+
+ rsinfo[j].dobj.objType = DO_ROW_SECURITY;
+ rsinfo[j].dobj.catId.tableoid =
+ atooid(PQgetvalue(res, j, i_tableoid));
+ rsinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
+ AssignDumpId(&rsinfo[j].dobj);
+ snprintf(namebuf, sizeof(namebuf), "row-security of %s",
+ tbinfo->rolname);
+ rsinfo[j].dobj.name = namebuf;
+ rsinfo[j].dobj.namespace = tbinfo->dobj.namespace;
+ rsinfo[j].rstable = tbinfo;
+ rsinfo[j].rseccmd = pg_strdup(PQgetvalue(res, j, i_rseccmd));
+ rsinfo[j].rsecqual = pg_strdup(PQgetvalue(res, j, i_rsecqual));
+ }
+ PQclear(res);
+ }
+ destroyPQExpBuffer(query);
+}
+
+/*
+ * dumpRowSecurity
+ * dump the definition of the given row-security policy
+ */
+static void
+dumpRowSecurity(Archive *fout, RowSecurityInfo *rsinfo)
+{
+ TableInfo *tbinfo = rsinfo->rstable;
+ PQExpBuffer query;
+ PQExpBuffer delqry;
+ const char *cmd;
+
+ if (dataOnly)
+ return;
+
+ query = createPQExpBuffer();
+ delqry = createPQExpBuffer();
+ appendPQExpBuffer(query, "ALTER TABLE %s SET ROW SECURITY ",
+ fmtId(tbinfo->dobj.name));
+ appendPQExpBuffer(delqry, "ALTER TABLE %s RESET ROW SECURITY ",
+ fmtId(tbinfo->dobj.name));
+ if (strcmp(rsinfo->rseccmd, "a") == 0)
+ cmd = "ALL";
+ else if (strcmp(rsinfo->rseccmd, "s") == 0)
+ cmd = "SELECT";
+ else if (strcmp(rsinfo->rseccmd, "i") == 0)
+ cmd = "INSERT";
+ else if (strcmp(rsinfo->rseccmd, "u") == 0)
+ cmd = "UPDATE";
+ else if (strcmp(rsinfo->rseccmd, "d") == 0)
+ cmd = "DELETE";
+ else
+ {
+ write_msg(NULL, "unexpected command type: '%s'\n", rsinfo->rseccmd);
+ exit_nicely(1);
+ }
+ appendPQExpBuffer(query, "FOR %s TO (%s)", cmd, rsinfo->rsecqual);
+ appendPQExpBuffer(delqry, "FOR %s", cmd);
+
+ ArchiveEntry(fout, rsinfo->dobj.catId, rsinfo->dobj.dumpId,
+ rsinfo->dobj.name,
+ rsinfo->dobj.namespace->dobj.name,
+ NULL,
+ tbinfo->rolname, false,
+ "ROW SECURITY", SECTION_POST_DATA,
+ query->data, delqry->data, NULL,
+ NULL, 0,
+ NULL, NULL);
+
+ destroyPQExpBuffer(query);
+}
+
static void
binary_upgrade_set_type_oids_by_type_oid(Archive *fout,
PQExpBuffer upgrade_buffer,
@@ -4214,6 +4343,7 @@ getTables(Archive *fout, int *numTables)
int i_relhastriggers;
int i_relhasindex;
int i_relhasrules;
+ int i_relhasrowsec;
int i_relhasoids;
int i_relfrozenxid;
int i_toastoid;
@@ -4251,7 +4381,45 @@ getTables(Archive *fout, int *numTables)
* we cannot correctly identify inherited columns, owned sequences, etc.
*/
- if (fout->remoteVersion >= 90300)
+ if (fout->remoteVersion >= 90400)
+ {
+ /*
+ * Left join to pick up dependency info linking sequences to their
+ * owning column, if any (note this dependency is AUTO as of 8.2)
+ */
+ appendPQExpBuffer(query,
+ "SELECT c.tableoid, c.oid, c.relname, "
+ "c.relacl, c.relkind, c.relnamespace, "
+ "(%s c.relowner) AS rolname, "
+ "c.relchecks, c.relhastriggers, "
+ "c.relhasindex, c.relhasrules, c.relhasoids, "
+ "c.relhasrowsecurity, "
+ "c.relfrozenxid, tc.oid AS toid, "
+ "tc.relfrozenxid AS tfrozenxid, "
+ "c.relpersistence, c.relispopulated, "
+ "c.relpages, "
+ "CASE WHEN c.reloftype <> 0 THEN c.reloftype::pg_catalog.regtype ELSE NULL END AS reloftype, "
+ "d.refobjid AS owning_tab, "
+ "d.refobjsubid AS owning_col, "
+ "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
+ "array_to_string(c.reloptions, ', ') AS reloptions, "
+ "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions "
+ "FROM pg_class c "
+ "LEFT JOIN pg_depend d ON "
+ "(c.relkind = '%c' AND "
+ "d.classid = c.tableoid AND d.objid = c.oid AND "
+ "d.objsubid = 0 AND "
+ "d.refclassid = c.tableoid AND d.deptype = 'a') "
+ "LEFT JOIN pg_class tc ON (c.reltoastrelid = tc.oid) "
+ "WHERE c.relkind in ('%c', '%c', '%c', '%c', '%c', '%c') "
+ "ORDER BY c.oid",
+ username_subquery,
+ RELKIND_SEQUENCE,
+ RELKIND_RELATION, RELKIND_SEQUENCE,
+ RELKIND_VIEW, RELKIND_COMPOSITE_TYPE,
+ RELKIND_MATVIEW, RELKIND_FOREIGN_TABLE);
+ }
+ else if (fout->remoteVersion >= 90300)
{
/*
* Left join to pick up dependency info linking sequences to their
@@ -4263,6 +4431,45 @@ getTables(Archive *fout, int *numTables)
"(%s c.relowner) AS rolname, "
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
+ "c.relfrozenxid, tc.oid AS toid, "
+ "tc.relfrozenxid AS tfrozenxid, "
+ "c.relpersistence, c.relispopulated, "
+ "c.relpages, "
+ "CASE WHEN c.reloftype <> 0 THEN c.reloftype::pg_catalog.regtype ELSE NULL END AS reloftype, "
+ "d.refobjid AS owning_tab, "
+ "d.refobjsubid AS owning_col, "
+ "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
+ "array_to_string(c.reloptions, ', ') AS reloptions, "
+ "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions "
+ "FROM pg_class c "
+ "LEFT JOIN pg_depend d ON "
+ "(c.relkind = '%c' AND "
+ "d.classid = c.tableoid AND d.objid = c.oid AND "
+ "d.objsubid = 0 AND "
+ "d.refclassid = c.tableoid AND d.deptype = 'a') "
+ "LEFT JOIN pg_class tc ON (c.reltoastrelid = tc.oid) "
+ "WHERE c.relkind in ('%c', '%c', '%c', '%c', '%c', '%c') "
+ "ORDER BY c.oid",
+ username_subquery,
+ RELKIND_SEQUENCE,
+ RELKIND_RELATION, RELKIND_SEQUENCE,
+ RELKIND_VIEW, RELKIND_COMPOSITE_TYPE,
+ RELKIND_MATVIEW, RELKIND_FOREIGN_TABLE);
+ }
+ else if (fout->remoteVersion >= 90100)
+ {
+ /*
+ * Left join to pick up dependency info linking sequences to their
+ * owning column, if any (note this dependency is AUTO as of 8.2)
+ */
+ appendPQExpBuffer(query,
+ "SELECT c.tableoid, c.oid, c.relname, "
+ "c.relacl, c.relkind, c.relnamespace, "
+ "(%s c.relowner) AS rolname, "
+ "c.relchecks, c.relhastriggers, "
+ "c.relhasindex, c.relhasrules, c.relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"c.relfrozenxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"c.relpersistence, c.relispopulated, "
@@ -4337,6 +4544,7 @@ getTables(Archive *fout, int *numTables)
"(%s c.relowner) AS rolname, "
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"c.relfrozenxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"'p' AS relpersistence, 't' as relispopulated, "
@@ -4373,6 +4581,7 @@ getTables(Archive *fout, int *numTables)
"(%s c.relowner) AS rolname, "
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"c.relfrozenxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"'p' AS relpersistence, 't' as relispopulated, "
@@ -4409,6 +4618,7 @@ getTables(Archive *fout, int *numTables)
"(%s c.relowner) AS rolname, "
"c.relchecks, (c.reltriggers <> 0) AS relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"c.relfrozenxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"'p' AS relpersistence, 't' as relispopulated, "
@@ -4445,6 +4655,7 @@ getTables(Archive *fout, int *numTables)
"(%s relowner) AS rolname, "
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"0 AS relfrozenxid, "
"0 AS toid, "
"0 AS tfrozenxid, "
@@ -4481,6 +4692,7 @@ getTables(Archive *fout, int *numTables)
"(%s relowner) AS rolname, "
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"0 AS relfrozenxid, "
"0 AS toid, "
"0 AS tfrozenxid, "
@@ -4513,6 +4725,7 @@ getTables(Archive *fout, int *numTables)
"(%s relowner) AS rolname, "
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"0 AS relfrozenxid, "
"0 AS toid, "
"0 AS tfrozenxid, "
@@ -4540,6 +4753,7 @@ getTables(Archive *fout, int *numTables)
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, "
"'t'::bool AS relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"0 AS relfrozenxid, "
"0 AS toid, "
"0 AS tfrozenxid, "
@@ -4577,6 +4791,7 @@ getTables(Archive *fout, int *numTables)
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, "
"'t'::bool AS relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"0 as relfrozenxid, "
"0 AS toid, "
"0 AS tfrozenxid, "
@@ -4624,6 +4839,7 @@ getTables(Archive *fout, int *numTables)
i_relhastriggers = PQfnumber(res, "relhastriggers");
i_relhasindex = PQfnumber(res, "relhasindex");
i_relhasrules = PQfnumber(res, "relhasrules");
+ i_relhasrowsec = PQfnumber(res, "relhasrowsecurity");
i_relhasoids = PQfnumber(res, "relhasoids");
i_relfrozenxid = PQfnumber(res, "relfrozenxid");
i_toastoid = PQfnumber(res, "toid");
@@ -4671,6 +4887,7 @@ getTables(Archive *fout, int *numTables)
tblinfo[i].hasindex = (strcmp(PQgetvalue(res, i, i_relhasindex), "t") == 0);
tblinfo[i].hasrules = (strcmp(PQgetvalue(res, i, i_relhasrules), "t") == 0);
tblinfo[i].hastriggers = (strcmp(PQgetvalue(res, i, i_relhastriggers), "t") == 0);
+ tblinfo[i].hasrowsec = (strcmp(PQgetvalue(res, i, i_relhasrowsec), "t") == 0);
tblinfo[i].hasoids = (strcmp(PQgetvalue(res, i, i_relhasoids), "t") == 0);
tblinfo[i].relispopulated = (strcmp(PQgetvalue(res, i, i_relispopulated), "t") == 0);
tblinfo[i].relpages = atoi(PQgetvalue(res, i, i_relpages));
@@ -7748,6 +7965,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
NULL, 0,
dumpBlobs, NULL);
break;
+ case DO_ROW_SECURITY:
+ dumpRowSecurity(fout, (RowSecurityInfo *) dobj);
+ break;
case DO_PRE_DATA_BOUNDARY:
case DO_POST_DATA_BOUNDARY:
/* never dumped, nothing to do */
@@ -14922,6 +15142,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
case DO_TRIGGER:
case DO_EVENT_TRIGGER:
case DO_DEFAULT_ACL:
+ case DO_ROW_SECURITY:
/* Post-data objects: must come after the post-data boundary */
addObjectDependency(dobj, postDataBound->dumpId);
break;
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 5582538..31e009a 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -111,7 +111,8 @@ typedef enum
DO_PRE_DATA_BOUNDARY,
DO_POST_DATA_BOUNDARY,
DO_EVENT_TRIGGER,
- DO_REFRESH_MATVIEW
+ DO_REFRESH_MATVIEW,
+ DO_ROW_SECURITY,
} DumpableObjectType;
typedef struct _dumpableObject
@@ -243,6 +244,7 @@ typedef struct _tableInfo
bool hasindex; /* does it have any indexes? */
bool hasrules; /* does it have any rules? */
bool hastriggers; /* does it have any triggers? */
+ bool hasrowsec; /* does it have any row-security policy? */
bool hasoids; /* does it have OIDs? */
uint32 frozenxid; /* for restore frozen xid */
Oid toast_oid; /* for restore toast frozen xid */
@@ -481,6 +483,14 @@ typedef struct _blobInfo
char *blobacl;
} BlobInfo;
+typedef struct _rowSecurityInfo
+{
+ DumpableObject dobj;
+ TableInfo *rstable;
+ char *rseccmd;
+ char *rsecqual;
+} RowSecurityInfo;
+
/* global decls */
extern bool force_quotes; /* double-quotes for identifiers flag */
extern bool g_verbose; /* verbose flag */
@@ -572,5 +582,6 @@ extern DefaultACLInfo *getDefaultACLs(Archive *fout, int *numDefaultACLs);
extern void getExtensionMembership(Archive *fout, ExtensionInfo extinfo[],
int numExtensions);
extern EventTriggerInfo *getEventTriggers(Archive *fout, int *numEventTriggers);
+extern void getRowSecurity(Archive *fout, TableInfo tblinfo[], int numTables);
#endif /* PG_DUMP_H */
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 141e713..5c5777c 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -1342,6 +1342,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
"BLOB DATA (ID %d)",
obj->dumpId);
return;
+ case DO_ROW_SECURITY:
+ snprintf(buf, bufsize,
+ "ROW-SECURITY POLICY (ID %d OID %u)",
+ obj->dumpId, obj->catId.oid);
+ return;
case DO_PRE_DATA_BOUNDARY:
snprintf(buf, bufsize,
"PRE-DATA BOUNDARY (ID %d)",
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 9b6b9c2..db5c0e5 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2698,6 +2698,10 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys
appendPQExpBuffer(&buf,
",\n pg_catalog.obj_description(c.oid, 'pg_class') as \"%s\"",
gettext_noop("Description"));
+ if (pset.sversion >= 90300)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_get_expr(rs.rsecqual, c.oid) as \"%s\"",
+ gettext_noop("Row-security"));
}
appendPQExpBuffer(&buf,
@@ -2707,6 +2711,9 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys
appendPQExpBuffer(&buf,
"\n LEFT JOIN pg_catalog.pg_index i ON i.indexrelid = c.oid"
"\n LEFT JOIN pg_catalog.pg_class c2 ON i.indrelid = c2.oid");
+ if (verbose && pset.sversion >= 90300)
+ appendPQExpBuffer(&buf,
+ "\n LEFT JOIN pg_rowsecurity rs ON rs.rsecrelid = c.oid");
appendPQExpBuffer(&buf, "\nWHERE c.relkind IN (");
if (showTables)
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index 3aefbb5e..c96e8d4 100644
--- a/src/include/catalog/dependency.h
+++ b/src/include/catalog/dependency.h
@@ -147,6 +147,7 @@ typedef enum ObjectClass
OCLASS_DEFACL, /* pg_default_acl */
OCLASS_EXTENSION, /* pg_extension */
OCLASS_EVENT_TRIGGER, /* pg_event_trigger */
+ OCLASS_ROWSECURITY, /* pg_rowsecurity */
MAX_OCLASS /* MUST BE LAST */
} ObjectClass;
diff --git a/src/include/catalog/indexing.h b/src/include/catalog/indexing.h
index 19268fb..331a64b 100644
--- a/src/include/catalog/indexing.h
+++ b/src/include/catalog/indexing.h
@@ -311,6 +311,11 @@ DECLARE_UNIQUE_INDEX(pg_extension_name_index, 3081, on pg_extension using btree(
DECLARE_UNIQUE_INDEX(pg_range_rngtypid_index, 3542, on pg_range using btree(rngtypid oid_ops));
#define RangeTypidIndexId 3542
+DECLARE_UNIQUE_INDEX(pg_rowsecurity_oid_index, 3819, on pg_rowsecurity using btree(oid oid_ops));
+#define RowSecurityOidIndexId 3819
+DECLARE_UNIQUE_INDEX(pg_rowsecurity_relid_index, 3839, on pg_rowsecurity using btree(rsecrelid oid_ops, rseccmd char_ops));
+#define RowSecurityRelidIndexId 3839
+
/* last step of initialization script: build the indexes declared above */
BUILD_INDICES
diff --git a/src/include/catalog/pg_class.h b/src/include/catalog/pg_class.h
index 49c4f6f..6a16819 100644
--- a/src/include/catalog/pg_class.h
+++ b/src/include/catalog/pg_class.h
@@ -64,6 +64,7 @@ CATALOG(pg_class,1259) BKI_BOOTSTRAP BKI_ROWTYPE_OID(83) BKI_SCHEMA_MACRO
bool relhaspkey; /* has (or has had) PRIMARY KEY index */
bool relhasrules; /* has (or has had) any rules */
bool relhastriggers; /* has (or has had) any TRIGGERs */
+ bool relhasrowsecurity; /* has (or has had) row-security policy */
bool relhassubclass; /* has (or has had) derived classes */
bool relispopulated; /* matview currently holds query results */
TransactionId relfrozenxid; /* all Xids < this are frozen in this rel */
@@ -93,7 +94,7 @@ typedef FormData_pg_class *Form_pg_class;
* ----------------
*/
-#define Natts_pg_class 28
+#define Natts_pg_class 29
#define Anum_pg_class_relname 1
#define Anum_pg_class_relnamespace 2
#define Anum_pg_class_reltype 3
@@ -116,12 +117,13 @@ typedef FormData_pg_class *Form_pg_class;
#define Anum_pg_class_relhaspkey 20
#define Anum_pg_class_relhasrules 21
#define Anum_pg_class_relhastriggers 22
-#define Anum_pg_class_relhassubclass 23
-#define Anum_pg_class_relispopulated 24
-#define Anum_pg_class_relfrozenxid 25
-#define Anum_pg_class_relminmxid 26
-#define Anum_pg_class_relacl 27
-#define Anum_pg_class_reloptions 28
+#define Anum_pg_class_relhasrowsecurity 23
+#define Anum_pg_class_relhassubclass 24
+#define Anum_pg_class_relispopulated 25
+#define Anum_pg_class_relfrozenxid 26
+#define Anum_pg_class_relminmxid 27
+#define Anum_pg_class_relacl 28
+#define Anum_pg_class_reloptions 29
/* ----------------
* initial contents of pg_class
@@ -136,13 +138,13 @@ typedef FormData_pg_class *Form_pg_class;
* Note: "3" in the relfrozenxid column stands for FirstNormalTransactionId;
* similarly, "1" in relminmxid stands for FirstMultiXactId
*/
-DATA(insert OID = 1247 ( pg_type PGNSP 71 0 PGUID 0 0 0 0 0 0 0 f f p r 30 0 t f f f f t 3 1 _null_ _null_ ));
+DATA(insert OID = 1247 ( pg_type PGNSP 71 0 PGUID 0 0 0 0 0 0 0 f f p r 30 0 t f f f f f t 3 1 _null_ _null_ ));
DESCR("");
-DATA(insert OID = 1249 ( pg_attribute PGNSP 75 0 PGUID 0 0 0 0 0 0 0 f f p r 21 0 f f f f f t 3 1 _null_ _null_ ));
+DATA(insert OID = 1249 ( pg_attribute PGNSP 75 0 PGUID 0 0 0 0 0 0 0 f f p r 21 0 f f f f f f t 3 1 _null_ _null_ ));
DESCR("");
-DATA(insert OID = 1255 ( pg_proc PGNSP 81 0 PGUID 0 0 0 0 0 0 0 f f p r 27 0 t f f f f t 3 1 _null_ _null_ ));
+DATA(insert OID = 1255 ( pg_proc PGNSP 81 0 PGUID 0 0 0 0 0 0 0 f f p r 27 0 t f f f f f t 3 1 _null_ _null_ ));
DESCR("");
-DATA(insert OID = 1259 ( pg_class PGNSP 83 0 PGUID 0 0 0 0 0 0 0 f f p r 28 0 t f f f f t 3 1 _null_ _null_ ));
+DATA(insert OID = 1259 ( pg_class PGNSP 83 0 PGUID 0 0 0 0 0 0 0 f f p r 29 0 t f f f f f t 3 1 _null_ _null_ ));
DESCR("");
diff --git a/src/include/catalog/pg_rowsecurity.h b/src/include/catalog/pg_rowsecurity.h
new file mode 100644
index 0000000..a068b35
--- /dev/null
+++ b/src/include/catalog/pg_rowsecurity.h
@@ -0,0 +1,76 @@
+/*
+ * pg_rowsecurity.h
+ * definition of the system catalog for row-security policy (pg_rowsecurity)
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ */
+#ifndef PG_ROWSECURITY_H
+#define PG_ROWSECURITY_H
+
+#include "catalog/genbki.h"
+#include "nodes/primnodes.h"
+#include "utils/memutils.h"
+#include "utils/relcache.h"
+
+/* ----------------
+ * pg_rowlevelsec definition. cpp turns this into
+ * typedef struct FormData_pg_rowlevelsec
+ * ----------------
+ */
+#define RowSecurityRelationId 3838
+
+CATALOG(pg_rowsecurity,3838)
+{
+ /* Oid of the relation that has row-security policy */
+ Oid rsecrelid;
+
+ /* One of ROWSECURITY_CMD_* below */
+ char rseccmd;
+#ifdef CATALOG_VARLEN
+ pg_node_tree rsecqual;
+#endif
+} FormData_pg_rowsecurity;
+
+/* ----------------
+ * Form_pg_rowlevelsec corresponds to a pointer to a row with
+ * the format of pg_rowlevelsec relation.
+ * ----------------
+ */
+typedef FormData_pg_rowsecurity *Form_pg_rowsecurity;
+
+/* ----------------
+ * compiler constants for pg_rowlevelsec
+ * ----------------
+ */
+#define Natts_pg_rowsecurity 3
+#define Anum_pg_rowsecurity_rsecrelid 1
+#define Anum_pg_rowsecurity_rseccmd 2
+#define Anum_pg_rowsecurity_rsecqual 3
+
+#define ROWSECURITY_CMD_ALL 'a'
+#define ROWSECURITY_CMD_SELECT 's'
+#define ROWSECURITY_CMD_INSERT 'i'
+#define ROWSECURITY_CMD_UPDATE 'u'
+#define ROWSECURITY_CMD_DELETE 'd'
+
+typedef struct
+{
+ Oid rsecid;
+ Expr *qual;
+ bool hassublinks;
+} RowSecurityEntry;
+
+typedef struct
+{
+ MemoryContext rscxt;
+ RowSecurityEntry rsall; /* row-security policy for ALL */
+} RowSecurityDesc;
+
+extern void RelationBuildRowSecurity(Relation relation);
+extern void ATExecSetRowSecurity(Relation relation,
+ const char *cmdname, Node *clause);
+extern void RemoveRowSecurityById(Oid relationId);
+
+#endif /* PG_ROWSECURITY_H */
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 48985b3..c182d2d 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -279,6 +279,7 @@ extern int trace_recovery(int trace_level);
/* flags to be OR'd to form sec_context */
#define SECURITY_LOCAL_USERID_CHANGE 0x0001
#define SECURITY_RESTRICTED_OPERATION 0x0002
+#define SECURITY_ROW_LEVEL_DISABLED 0x0004
extern char *DatabasePath;
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 4f77016..f49cbf9 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -306,6 +306,8 @@ typedef struct JunkFilter
* ConstraintExprs array of constraint-checking expr states
* junkFilter for removing junk attributes from tuples
* projectReturning for computing a RETURNING list
+ * rowSecurity for row-security checks
+ * rowSecParams param-list if row-security has SubLink
* ----------------
*/
typedef struct ResultRelInfo
@@ -325,6 +327,8 @@ typedef struct ResultRelInfo
List **ri_ConstraintExprs;
JunkFilter *ri_junkFilter;
ProjectionInfo *ri_projectReturning;
+ Node *ri_rowSecurity;
+ List *ri_rowSecParams;
} ResultRelInfo;
/* ----------------
diff --git a/src/include/nodes/nodeFuncs.h b/src/include/nodes/nodeFuncs.h
index d4901ca..cb30110 100644
--- a/src/include/nodes/nodeFuncs.h
+++ b/src/include/nodes/nodeFuncs.h
@@ -24,6 +24,7 @@
#define QTW_IGNORE_RANGE_TABLE 0x08 /* skip rangetable entirely */
#define QTW_EXAMINE_RTES 0x10 /* examine RTEs */
#define QTW_DONT_COPY_QUERY 0x20 /* do not copy top Query */
+#define QTW_IGNORE_RETURNING 0x40 /* skip returning clause */
extern Oid exprType(const Node *expr);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index de22dff..2b81e27 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -31,7 +31,8 @@ typedef enum QuerySource
QSRC_PARSER, /* added by parse analysis (now unused) */
QSRC_INSTEAD_RULE, /* added by unconditional INSTEAD rule */
QSRC_QUAL_INSTEAD_RULE, /* added by conditional INSTEAD rule */
- QSRC_NON_INSTEAD_RULE /* added by non-INSTEAD rule */
+ QSRC_NON_INSTEAD_RULE, /* added by non-INSTEAD rule */
+ QSRC_ROW_SECURITY, /* added by row-security */
} QuerySource;
/* Sort ordering options for ORDER BY and CREATE INDEX */
@@ -112,7 +113,9 @@ typedef struct Query
int resultRelation; /* rtable index of target relation for
* INSERT/UPDATE/DELETE; 0 for SELECT */
-
+ int sourceRelation; /* rtable index of source relation for
+ * UPDATE/DELETE, if not identical with
+ * resultRelation; 0 for elsewhere */
bool hasAggs; /* has aggregates in tlist or havingQual */
bool hasWindowFuncs; /* has window functions in tlist */
bool hasSubLinks; /* has subquery SubLink */
@@ -724,6 +727,11 @@ typedef struct RangeTblEntry
*/
Query *subquery; /* the sub-query */
bool security_barrier; /* is from security_barrier view? */
+ Oid rowsec_relid; /* OID of the original relation, if this
+ * sub-query originated from row-security
+ * policy on the relation. Elsewhere, it
+ * should be InvalidOid.
+ */
/*
* Fields valid for a join RTE (else NULL/zero):
@@ -1250,6 +1258,8 @@ typedef enum AlterTableType
AT_DropInherit, /* NO INHERIT parent */
AT_AddOf, /* OF <type_name> */
AT_DropOf, /* NOT OF */
+ AT_SetRowSecurity, /* SET ROW SECURITY (...) */
+ AT_ResetRowSecurity, /* RESET ROW SECURITY */
AT_GenericOptions /* OPTIONS (...) */
} AlterTableType;
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 841701e..cc94421 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -67,6 +67,8 @@ typedef struct PlannedStmt
List *invalItems; /* other dependencies, as PlanInvalItems */
int nParamExec; /* number of PARAM_EXEC Params used */
+
+ Oid planUserId; /* user-id this plan assumed, or InvalidOid */
} PlannedStmt;
/* macro for fetching the Plan associated with a SubPlan node */
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index c0a636b..ed01b65 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -98,6 +98,8 @@ typedef struct PlannerGlobal
Index lastRowMarkId; /* highest PlanRowMark ID assigned */
bool transientPlan; /* redo plan when TransactionXmin changes? */
+
+ Oid planUserId; /* User-Id to be assumed on this plan */
} PlannerGlobal;
/* macro for fetching the Plan associated with a SubPlan node */
@@ -1416,6 +1418,10 @@ typedef struct AppendRelInfo
*/
Index parent_relid; /* RT index of append parent rel */
Index child_relid; /* RT index of append child rel */
+ Index child_result; /* RT index of append child rel's source,
+ * if source of result relation is not
+ * identical. Elsewhere, 0.
+ */
/*
* For an inheritance appendrel, the parent and child are both regular
diff --git a/src/include/optimizer/rowsecurity.h b/src/include/optimizer/rowsecurity.h
new file mode 100644
index 0000000..ff4dd9a
--- /dev/null
+++ b/src/include/optimizer/rowsecurity.h
@@ -0,0 +1,27 @@
+/* -------------------------------------------------------------------------
+ *
+ * rowsecurity.h
+ * prototypes for optimizer/rowsecurity.c
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * -------------------------------------------------------------------------
+ */
+#ifndef ROWSECURITY_H
+#define ROWSECURITY_H
+
+#include "nodes/execnodes.h"
+#include "nodes/parsenodes.h"
+#include "nodes/relation.h"
+#include "utils/rel.h"
+
+typedef List *(*row_security_policy_hook_type)(CmdType cmdtype,
+ Relation relation);
+extern PGDLLIMPORT row_security_policy_hook_type row_security_policy_hook;
+
+extern bool copy_row_security_policy(CopyStmt *stmt,
+ Relation relation, List *attnums);
+extern void apply_row_security_policy(PlannerInfo *root);
+
+#endif /* ROWSECURITY_H */
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index 49ca764..a3d5be1 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -62,7 +62,8 @@ typedef enum ParseExprKind
EXPR_KIND_INDEX_PREDICATE, /* index predicate */
EXPR_KIND_ALTER_COL_TRANSFORM, /* transform expr in ALTER COLUMN TYPE */
EXPR_KIND_EXECUTE_PARAMETER, /* parameter value in EXECUTE */
- EXPR_KIND_TRIGGER_WHEN /* WHEN condition in CREATE TRIGGER */
+ EXPR_KIND_TRIGGER_WHEN, /* WHEN condition in CREATE TRIGGER */
+ EXPR_KIND_ROW_SECURITY, /* ROW SECURITY policy for a table */
} ParseExprKind;
diff --git a/src/include/parser/parsetree.h b/src/include/parser/parsetree.h
index 9608cc6..b4e9df4 100644
--- a/src/include/parser/parsetree.h
+++ b/src/include/parser/parsetree.h
@@ -32,14 +32,11 @@
((RangeTblEntry *) list_nth(rangetable, (rangetable_index)-1))
/*
- * getrelid
- *
* Given the range index of a relation, return the corresponding
* relation OID. Note that InvalidOid will be returned if the
* RTE is for a non-relation-type RTE.
*/
-#define getrelid(rangeindex,rangetable) \
- (rt_fetch(rangeindex, rangetable)->relid)
+extern Oid getrelid(Index rangeindex, List *rangetable);
/*
* Given an RTE and an attribute number, return the appropriate
diff --git a/src/include/rewrite/rewriteHandler.h b/src/include/rewrite/rewriteHandler.h
index 1831de4..ef54c57 100644
--- a/src/include/rewrite/rewriteHandler.h
+++ b/src/include/rewrite/rewriteHandler.h
@@ -18,6 +18,7 @@
#include "nodes/parsenodes.h"
extern List *QueryRewrite(Query *parsetree);
+extern void QueryRewriteExpr(Node *node, List *activeRIRs);
extern void AcquireRewriteLocks(Query *parsetree, bool forUpdatePushedDown);
extern Node *build_column_default(Relation rel, int attrno);
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index 72f8491..35819fa 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -128,6 +128,8 @@ typedef struct CachedPlan
bool is_oneshot; /* is it a "oneshot" plan? */
bool is_saved; /* is CachedPlan in a long-lived context? */
bool is_valid; /* is the stmt_list currently valid? */
+ Oid planUserId; /* is user-id that is assumed on this cached
+ plan, or InvalidOid if portable for anybody */
TransactionId saved_xmin; /* if valid, replan when TransactionXmin
* changes from this value */
int generation; /* parent's generation number for this plan */
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 58cc3f7..5e8441c 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -18,6 +18,7 @@
#include "catalog/pg_am.h"
#include "catalog/pg_class.h"
#include "catalog/pg_index.h"
+#include "catalog/pg_rowsecurity.h"
#include "fmgr.h"
#include "nodes/bitmapset.h"
#include "rewrite/prs2lock.h"
@@ -109,6 +110,7 @@ typedef struct RelationData
RuleLock *rd_rules; /* rewrite rules */
MemoryContext rd_rulescxt; /* private memory cxt for rd_rules, if any */
TriggerDesc *trigdesc; /* Trigger info, or NULL if rel has none */
+ RowSecurityDesc *rsdesc; /* Row-security policy, or NULL */
/*
* rd_options is set whenever rd_rel is loaded into the relcache entry.
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
new file mode 100644
index 0000000..04591df
--- /dev/null
+++ b/src/test/regress/expected/rowsecurity.out
@@ -0,0 +1,950 @@
+--
+-- Test of Row-level security feature
+--
+-- Clean up in case a prior regression run failed
+-- Suppress NOTICE messages when users/groups don't exist
+SET client_min_messages TO 'warning';
+DROP USER IF EXISTS rls_regress_user0;
+DROP USER IF EXISTS rls_regress_user1;
+DROP USER IF EXISTS rls_regress_user2;
+DROP SCHEMA IF EXISTS rls_regress_schema CASCADE;
+RESET client_min_messages;
+-- initial setup
+CREATE USER rls_regress_user0;
+CREATE USER rls_regress_user1;
+CREATE USER rls_regress_user2;
+CREATE SCHEMA rls_regress_schema;
+GRANT ALL ON SCHEMA rls_regress_schema TO public;
+SET search_path = rls_regress_schema;
+-- setup of malicious function
+CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool
+ COST 0.0000001 LANGUAGE plpgsql
+ AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END';
+GRANT EXECUTE ON FUNCTION f_leak(text) TO public;
+-- BASIC Row-Level Security Scenario
+SET SESSION AUTHORIZATION rls_regress_user0;
+CREATE TABLE uaccount (
+ pguser name primary key,
+ seclv int
+);
+INSERT INTO uaccount VALUES
+ ('rls_regress_user0', 99),
+ ('rls_regress_user1', 1),
+ ('rls_regress_user2', 2),
+ ('rls_regress_user3', 3);
+GRANT SELECT ON uaccount TO public;
+CREATE TABLE category (
+ cid int primary key,
+ cname text
+);
+GRANT ALL ON category TO public;
+INSERT INTO category VALUES
+ (11, 'novel'),
+ (22, 'science fiction'),
+ (33, 'technology'),
+ (44, 'manga');
+CREATE TABLE document (
+ did int primary key,
+ cid int references category(cid),
+ dlevel int not null,
+ dauthor name,
+ dtitle text
+);
+GRANT ALL ON document TO public;
+INSERT INTO document VALUES
+ ( 1, 11, 1, 'rls_regress_user1', 'my first novel'),
+ ( 2, 11, 2, 'rls_regress_user1', 'my second novel'),
+ ( 3, 22, 2, 'rls_regress_user1', 'my science fiction'),
+ ( 4, 44, 1, 'rls_regress_user1', 'my first manga'),
+ ( 5, 44, 2, 'rls_regress_user1', 'my second manga'),
+ ( 6, 22, 1, 'rls_regress_user2', 'great science fiction'),
+ ( 7, 33, 2, 'rls_regress_user2', 'great technology book'),
+ ( 8, 44, 1, 'rls_regress_user2', 'great manga');
+-- user's security level must higher than or equal to document's one
+ALTER TABLE document SET ROW SECURITY FOR ALL
+ TO (dlevel <= (SELECT seclv FROM uaccount WHERE pguser = current_user));
+-- viewpoint from rls_regress_user1
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE: f_leak => my first novel
+NOTICE: f_leak => my first manga
+NOTICE: f_leak => great science fiction
+NOTICE: f_leak => great manga
+ did | cid | dlevel | dauthor | dtitle
+-----+-----+--------+-------------------+-----------------------
+ 1 | 11 | 1 | rls_regress_user1 | my first novel
+ 4 | 44 | 1 | rls_regress_user1 | my first manga
+ 6 | 22 | 1 | rls_regress_user2 | great science fiction
+ 8 | 44 | 1 | rls_regress_user2 | great manga
+(4 rows)
+
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+NOTICE: f_leak => my first novel
+NOTICE: f_leak => my first manga
+NOTICE: f_leak => great science fiction
+NOTICE: f_leak => great manga
+ cid | did | dlevel | dauthor | dtitle | cname
+-----+-----+--------+-------------------+-----------------------+-----------------
+ 11 | 1 | 1 | rls_regress_user1 | my first novel | novel
+ 22 | 6 | 1 | rls_regress_user2 | great science fiction | science fiction
+ 44 | 8 | 1 | rls_regress_user2 | great manga | manga
+ 44 | 4 | 1 | rls_regress_user1 | my first manga | manga
+(4 rows)
+
+-- viewpoint from rls_regress_user2
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE: f_leak => my first novel
+NOTICE: f_leak => my second novel
+NOTICE: f_leak => my science fiction
+NOTICE: f_leak => my first manga
+NOTICE: f_leak => my second manga
+NOTICE: f_leak => great science fiction
+NOTICE: f_leak => great technology book
+NOTICE: f_leak => great manga
+ did | cid | dlevel | dauthor | dtitle
+-----+-----+--------+-------------------+-----------------------
+ 1 | 11 | 1 | rls_regress_user1 | my first novel
+ 2 | 11 | 2 | rls_regress_user1 | my second novel
+ 3 | 22 | 2 | rls_regress_user1 | my science fiction
+ 4 | 44 | 1 | rls_regress_user1 | my first manga
+ 5 | 44 | 2 | rls_regress_user1 | my second manga
+ 6 | 22 | 1 | rls_regress_user2 | great science fiction
+ 7 | 33 | 2 | rls_regress_user2 | great technology book
+ 8 | 44 | 1 | rls_regress_user2 | great manga
+(8 rows)
+
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+NOTICE: f_leak => my first novel
+NOTICE: f_leak => my second novel
+NOTICE: f_leak => my science fiction
+NOTICE: f_leak => my first manga
+NOTICE: f_leak => my second manga
+NOTICE: f_leak => great science fiction
+NOTICE: f_leak => great technology book
+NOTICE: f_leak => great manga
+ cid | did | dlevel | dauthor | dtitle | cname
+-----+-----+--------+-------------------+-----------------------+-----------------
+ 11 | 2 | 2 | rls_regress_user1 | my second novel | novel
+ 11 | 1 | 1 | rls_regress_user1 | my first novel | novel
+ 22 | 6 | 1 | rls_regress_user2 | great science fiction | science fiction
+ 22 | 3 | 2 | rls_regress_user1 | my science fiction | science fiction
+ 33 | 7 | 2 | rls_regress_user2 | great technology book | technology
+ 44 | 8 | 1 | rls_regress_user2 | great manga | manga
+ 44 | 5 | 2 | rls_regress_user1 | my second manga | manga
+ 44 | 4 | 1 | rls_regress_user1 | my first manga | manga
+(8 rows)
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+ QUERY PLAN
+----------------------------------------------------------
+ Subquery Scan on document
+ Filter: f_leak(document.dtitle)
+ -> Seq Scan on document document_1
+ Filter: (dlevel <= $0)
+ InitPlan 1 (returns $0)
+ -> Index Scan using uaccount_pkey on uaccount
+ Index Cond: (pguser = "current_user"())
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+ QUERY PLAN
+----------------------------------------------------------------------
+ Hash Join
+ Hash Cond: (category.cid = document.cid)
+ -> Seq Scan on category
+ -> Hash
+ -> Subquery Scan on document
+ Filter: f_leak(document.dtitle)
+ -> Seq Scan on document document_1
+ Filter: (dlevel <= $0)
+ InitPlan 1 (returns $0)
+ -> Index Scan using uaccount_pkey on uaccount
+ Index Cond: (pguser = "current_user"())
+(11 rows)
+
+-- only owner can change row-level security
+ALTER TABLE document SET ROW SECURITY FOR ALL TO (true); -- fail
+ERROR: must be owner of relation document
+ALTER TABLE document RESET ROW SECURITY FOR ALL; -- fail
+ERROR: must be owner of relation document
+SET SESSION AUTHORIZATION rls_regress_user0;
+ALTER TABLE document SET ROW SECURITY FOR ALL TO (dauthor = current_user);
+-- viewpoint from rls_regress_user1 again
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE: f_leak => my first novel
+NOTICE: f_leak => my second novel
+NOTICE: f_leak => my science fiction
+NOTICE: f_leak => my first manga
+NOTICE: f_leak => my second manga
+ did | cid | dlevel | dauthor | dtitle
+-----+-----+--------+-------------------+--------------------
+ 1 | 11 | 1 | rls_regress_user1 | my first novel
+ 2 | 11 | 2 | rls_regress_user1 | my second novel
+ 3 | 22 | 2 | rls_regress_user1 | my science fiction
+ 4 | 44 | 1 | rls_regress_user1 | my first manga
+ 5 | 44 | 2 | rls_regress_user1 | my second manga
+(5 rows)
+
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+NOTICE: f_leak => my first novel
+NOTICE: f_leak => my second novel
+NOTICE: f_leak => my science fiction
+NOTICE: f_leak => my first manga
+NOTICE: f_leak => my second manga
+ cid | did | dlevel | dauthor | dtitle | cname
+-----+-----+--------+-------------------+--------------------+-----------------
+ 11 | 1 | 1 | rls_regress_user1 | my first novel | novel
+ 11 | 2 | 2 | rls_regress_user1 | my second novel | novel
+ 22 | 3 | 2 | rls_regress_user1 | my science fiction | science fiction
+ 44 | 4 | 1 | rls_regress_user1 | my first manga | manga
+ 44 | 5 | 2 | rls_regress_user1 | my second manga | manga
+(5 rows)
+
+-- viewpoint from rls_regress_user2 again
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE: f_leak => great science fiction
+NOTICE: f_leak => great technology book
+NOTICE: f_leak => great manga
+ did | cid | dlevel | dauthor | dtitle
+-----+-----+--------+-------------------+-----------------------
+ 6 | 22 | 1 | rls_regress_user2 | great science fiction
+ 7 | 33 | 2 | rls_regress_user2 | great technology book
+ 8 | 44 | 1 | rls_regress_user2 | great manga
+(3 rows)
+
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+NOTICE: f_leak => great science fiction
+NOTICE: f_leak => great technology book
+NOTICE: f_leak => great manga
+ cid | did | dlevel | dauthor | dtitle | cname
+-----+-----+--------+-------------------+-----------------------+-----------------
+ 22 | 6 | 1 | rls_regress_user2 | great science fiction | science fiction
+ 33 | 7 | 2 | rls_regress_user2 | great technology book | technology
+ 44 | 8 | 1 | rls_regress_user2 | great manga | manga
+(3 rows)
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+ QUERY PLAN
+----------------------------------------------
+ Subquery Scan on document
+ Filter: f_leak(document.dtitle)
+ -> Seq Scan on document document_1
+ Filter: (dauthor = "current_user"())
+(4 rows)
+
+EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+ QUERY PLAN
+----------------------------------------------------
+ Nested Loop
+ -> Subquery Scan on document
+ Filter: f_leak(document.dtitle)
+ -> Seq Scan on document document_1
+ Filter: (dauthor = "current_user"())
+ -> Index Scan using category_pkey on category
+ Index Cond: (cid = document.cid)
+(7 rows)
+
+-- interaction of FK/PK constraints
+SET SESSION AUTHORIZATION rls_regress_user0;
+ALTER TABLE category SET ROW SECURITY FOR ALL
+ TO (CASE WHEN current_user = 'rls_regress_user1' THEN cid IN (11, 33)
+ WHEN current_user = 'rls_regress_user2' THEN cid IN (22, 44)
+ ELSE false END);
+-- cannot delete PK referenced by invisible FK
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document d full outer join category c on d.cid = c.cid;
+ did | cid | dlevel | dauthor | dtitle | cid | cname
+-----+-----+--------+-------------------+--------------------+-----+------------
+ 2 | 11 | 2 | rls_regress_user1 | my second novel | 11 | novel
+ 1 | 11 | 1 | rls_regress_user1 | my first novel | 11 | novel
+ | | | | | 33 | technology
+ 5 | 44 | 2 | rls_regress_user1 | my second manga | |
+ 4 | 44 | 1 | rls_regress_user1 | my first manga | |
+ 3 | 22 | 2 | rls_regress_user1 | my science fiction | |
+(6 rows)
+
+DELETE FROM category WHERE cid = 33; -- failed
+ERROR: update or delete on table "category" violates foreign key constraint "document_cid_fkey" on table "document"
+DETAIL: Key (cid)=(33) is still referenced from table "document".
+-- cannot insert FK referencing invisible PK
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document d full outer join category c on d.cid = c.cid;
+ did | cid | dlevel | dauthor | dtitle | cid | cname
+-----+-----+--------+-------------------+-----------------------+-----+-----------------
+ 6 | 22 | 1 | rls_regress_user2 | great science fiction | 22 | science fiction
+ 8 | 44 | 1 | rls_regress_user2 | great manga | 44 | manga
+ 7 | 33 | 2 | rls_regress_user2 | great technology book | |
+(3 rows)
+
+INSERT INTO document VALUES (10, 33, 1, current_user, 'hoge'); -- failed
+ERROR: insert or update on table "document" violates foreign key constraint "document_cid_fkey"
+DETAIL: Key (cid)=(33) is not present in table "category".
+-- database superuser can bypass RLS policy
+RESET SESSION AUTHORIZATION;
+SELECT * FROM document;
+ did | cid | dlevel | dauthor | dtitle
+-----+-----+--------+-------------------+-----------------------
+ 1 | 11 | 1 | rls_regress_user1 | my first novel
+ 2 | 11 | 2 | rls_regress_user1 | my second novel
+ 3 | 22 | 2 | rls_regress_user1 | my science fiction
+ 4 | 44 | 1 | rls_regress_user1 | my first manga
+ 5 | 44 | 2 | rls_regress_user1 | my second manga
+ 6 | 22 | 1 | rls_regress_user2 | great science fiction
+ 7 | 33 | 2 | rls_regress_user2 | great technology book
+ 8 | 44 | 1 | rls_regress_user2 | great manga
+(8 rows)
+
+SELECT * FROM category;
+ cid | cname
+-----+-----------------
+ 11 | novel
+ 22 | science fiction
+ 33 | technology
+ 44 | manga
+(4 rows)
+
+--
+-- Table inheritance and RLS policy
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+CREATE TABLE t1 (a int, junk1 text, b text) WITH OIDS;
+ALTER TABLE t1 DROP COLUMN junk1; -- just a disturbing factor
+GRANT ALL ON t1 TO public;
+COPY t1 FROM stdin WITH (oids);
+CREATE TABLE t2 (c float) INHERITS (t1);
+COPY t2 FROM stdin WITH (oids);
+CREATE TABLE t3 (c text, b text, a int) WITH OIDS;
+ALTER TABLE t3 INHERIT t1;
+COPY t3(a,b,c) FROM stdin WITH (oids);
+ALTER TABLE t1 SET ROW SECURITY FOR ALL TO (a % 2 = 0); -- be even number
+ALTER TABLE t2 SET ROW SECURITY FOR ALL TO (a % 2 = 1); -- be odd number
+SELECT * FROM t1;
+ a | b
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1;
+ QUERY PLAN
+-------------------------------------
+ Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+(8 rows)
+
+SELECT * FROM t1 WHERE f_leak(b);
+NOTICE: f_leak => bbb
+NOTICE: f_leak => ddd
+NOTICE: f_leak => abc
+NOTICE: f_leak => cde
+NOTICE: f_leak => xxx
+NOTICE: f_leak => yyy
+NOTICE: f_leak => zzz
+ a | b
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+ QUERY PLAN
+-------------------------------------
+ Append
+ -> Subquery Scan on t1
+ Filter: f_leak(t1.b)
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ Filter: f_leak(t2.b)
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+ Filter: f_leak(b)
+(11 rows)
+
+-- reference to system column
+SELECT oid, * FROM t1;
+ oid | a | b
+-----+---+-----
+ 102 | 2 | bbb
+ 104 | 4 | ddd
+ 201 | 1 | abc
+ 203 | 3 | cde
+ 301 | 1 | xxx
+ 302 | 2 | yyy
+ 303 | 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1;
+ QUERY PLAN
+-------------------------------------
+ Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+(8 rows)
+
+-- reference to whole-row reference
+SELECT *,t1 FROM t1;
+ a | b | t1
+---+-----+---------
+ 2 | bbb | (2,bbb)
+ 4 | ddd | (4,ddd)
+ 1 | abc | (1,abc)
+ 3 | cde | (3,cde)
+ 1 | xxx | (1,xxx)
+ 2 | yyy | (2,yyy)
+ 3 | zzz | (3,zzz)
+(7 rows)
+
+EXPLAIN (costs off) SELECT *,t1 FROM t1;
+ QUERY PLAN
+-------------------------------------
+ Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+(8 rows)
+
+-- for share/update lock
+SELECT * FROM t1 FOR SHARE;
+ a | b
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 FOR SHARE;
+ QUERY PLAN
+-------------------------------------------
+ LockRows
+ -> Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+(9 rows)
+
+SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+NOTICE: f_leak => bbb
+NOTICE: f_leak => ddd
+NOTICE: f_leak => abc
+NOTICE: f_leak => cde
+NOTICE: f_leak => xxx
+NOTICE: f_leak => yyy
+NOTICE: f_leak => zzz
+ a | b
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+ QUERY PLAN
+-------------------------------------------
+ LockRows
+ -> Append
+ -> Subquery Scan on t1
+ Filter: f_leak(t1.b)
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ Filter: f_leak(t2.b)
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+ Filter: f_leak(b)
+(12 rows)
+
+--
+-- COPY TO statement
+--
+COPY t1 TO stdout;
+2 bbb
+4 ddd
+COPY t1 TO stdout WITH OIDS;
+102 2 bbb
+104 4 ddd
+COPY t2(c,b) TO stdout WITH OIDS;
+201 1.1 abc
+203 3.3 cde
+COPY (SELECT * FROM t1) TO stdout;
+2 bbb
+4 ddd
+1 abc
+3 cde
+1 xxx
+2 yyy
+3 zzz
+COPY document TO stdout WITH OIDS; -- failed (no oid column)
+ERROR: table "document" does not have OIDs
+--
+-- recursive RLS and VIEWs in policy
+--
+CREATE TABLE s1 (a int, b text);
+INSERT INTO s1 (SELECT x, md5(x::text) FROM generate_series(-10,10) x);
+CREATE TABLE s2 (x int, y text);
+INSERT INTO s2 (SELECT x, md5(x::text) FROM generate_series(-6,6) x);
+CREATE VIEW v2 AS SELECT * FROM s2 WHERE y like '%af%';
+ALTER TABLE s1 SET ROW SECURITY FOR ALL
+ TO (a in (select x from s2 where y like '%2f%'));
+ALTER TABLE s2 SET ROW SECURITY FOR ALL
+ TO (x in (select a from s1 where b like '%22%'));
+SELECT * FROM s1 WHERE f_leak(b); -- fail (infinite recursion)
+ERROR: infinite recursion detected for relation "s1"
+ALTER TABLE s2 SET ROW SECURITY FOR ALL TO (x % 2 = 0);
+SELECT * FROM s1 WHERE f_leak(b); -- OK
+NOTICE: f_leak => c81e728d9d4c2f636f067f89cc14862c
+NOTICE: f_leak => a87ff679a2f3e71d9181a67b7542122c
+ a | b
+---+----------------------------------
+ 2 | c81e728d9d4c2f636f067f89cc14862c
+ 4 | a87ff679a2f3e71d9181a67b7542122c
+(2 rows)
+
+EXPLAIN SELECT * FROM only s1 WHERE f_leak(b);
+ QUERY PLAN
+---------------------------------------------------------------------------------------
+ Subquery Scan on s1 (cost=28.55..61.67 rows=205 width=36)
+ Filter: f_leak(s1.b)
+ -> Hash Join (cost=28.55..55.52 rows=615 width=36)
+ Hash Cond: (s1_1.a = s2.x)
+ -> Seq Scan on s1 s1_1 (cost=0.00..22.30 rows=1230 width=36)
+ -> Hash (cost=28.54..28.54 rows=1 width=4)
+ -> HashAggregate (cost=28.53..28.54 rows=1 width=4)
+ -> Subquery Scan on s2 (cost=0.00..28.52 rows=1 width=4)
+ Filter: (s2.y ~~ '%2f%'::text)
+ -> Seq Scan on s2 s2_1 (cost=0.00..28.45 rows=6 width=36)
+ Filter: ((x % 2) = 0)
+(11 rows)
+
+ALTER TABLE s1 SET ROW SECURITY FOR ALL
+ TO (a in (select x from v2)); -- using VIEW in RLS policy
+SELECT * FROM s1 WHERE f_leak(b); -- OK
+NOTICE: f_leak => 0267aaf632e87a63288a08331f22c7c3
+NOTICE: f_leak => 1679091c5a880faf6fb5e6087eb1b2dc
+ a | b
+----+----------------------------------
+ -4 | 0267aaf632e87a63288a08331f22c7c3
+ 6 | 1679091c5a880faf6fb5e6087eb1b2dc
+(2 rows)
+
+EXPLAIN (COSTS OFF) SELECT * FROM s1 WHERE f_leak(b);
+ QUERY PLAN
+----------------------------------------------------------
+ Subquery Scan on s1
+ Filter: f_leak(s1.b)
+ -> Hash Join
+ Hash Cond: (s1_1.a = s2.x)
+ -> Seq Scan on s1 s1_1
+ -> Hash
+ -> HashAggregate
+ -> Subquery Scan on s2
+ Filter: (s2.y ~~ '%af%'::text)
+ -> Seq Scan on s2 s2_1
+ Filter: ((x % 2) = 0)
+(11 rows)
+
+SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+ xx | x | y
+----+----+----------------------------------
+ -6 | -6 | 596a3d04481816330f07e4f97510c28f
+ -4 | -4 | 0267aaf632e87a63288a08331f22c7c3
+ 2 | 2 | c81e728d9d4c2f636f067f89cc14862c
+(3 rows)
+
+EXPLAIN (COSTS OFF) SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+ QUERY PLAN
+--------------------------------------------------------------------
+ Subquery Scan on s2
+ Filter: (s2.y ~~ '%28%'::text)
+ -> Seq Scan on s2 s2_1
+ Filter: ((x % 2) = 0)
+ SubPlan 1
+ -> Limit
+ -> Subquery Scan on s1
+ -> Nested Loop Semi Join
+ Join Filter: (s1_1.a = s2_2.x)
+ -> Seq Scan on s1 s1_1
+ -> Materialize
+ -> Subquery Scan on s2_2
+ Filter: (s2_2.y ~~ '%af%'::text)
+ -> Seq Scan on s2 s2_3
+ Filter: ((x % 2) = 0)
+(15 rows)
+
+ALTER TABLE s2 SET ROW SECURITY FOR ALL
+ TO (x in (select a from s1 where b like '%d2%'));
+SELECT * FROM s1 WHERE f_leak(b); -- fail (infinite recursion via view)
+ERROR: infinite recursion detected for relation "s1"
+-- prepared statement with rls_regress_user0 privilege
+PREPARE p1(int) AS SELECT * FROM t1 WHERE a <= $1;
+EXECUTE p1(2);
+ a | b
+---+-----
+ 2 | bbb
+ 1 | abc
+ 1 | xxx
+ 2 | yyy
+(4 rows)
+
+EXPLAIN (costs off) EXECUTE p1(2);
+ QUERY PLAN
+----------------------------------------------------
+ Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a <= 2) AND ((a % 2) = 0))
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a <= 2) AND ((a % 2) = 1))
+ -> Seq Scan on t3
+ Filter: (a <= 2)
+(9 rows)
+
+-- superuser is allowed to bypass RLS checks
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1 WHERE f_leak(b);
+NOTICE: f_leak => aaa
+NOTICE: f_leak => bbb
+NOTICE: f_leak => ccc
+NOTICE: f_leak => ddd
+NOTICE: f_leak => abc
+NOTICE: f_leak => bcd
+NOTICE: f_leak => cde
+NOTICE: f_leak => def
+NOTICE: f_leak => xxx
+NOTICE: f_leak => yyy
+NOTICE: f_leak => zzz
+ a | b
+---+-----
+ 1 | aaa
+ 2 | bbb
+ 3 | ccc
+ 4 | ddd
+ 1 | abc
+ 2 | bcd
+ 3 | cde
+ 4 | def
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(11 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+ QUERY PLAN
+---------------------------
+ Append
+ -> Seq Scan on t1
+ Filter: f_leak(b)
+ -> Seq Scan on t2
+ Filter: f_leak(b)
+ -> Seq Scan on t3
+ Filter: f_leak(b)
+(7 rows)
+
+-- plan cache should be invalidated
+EXECUTE p1(2);
+ a | b
+---+-----
+ 1 | aaa
+ 2 | bbb
+ 1 | abc
+ 2 | bcd
+ 1 | xxx
+ 2 | yyy
+(6 rows)
+
+EXPLAIN (costs off) EXECUTE p1(2);
+ QUERY PLAN
+--------------------------
+ Append
+ -> Seq Scan on t1
+ Filter: (a <= 2)
+ -> Seq Scan on t2
+ Filter: (a <= 2)
+ -> Seq Scan on t3
+ Filter: (a <= 2)
+(7 rows)
+
+PREPARE p2(int) AS SELECT * FROM t1 WHERE a = $1;
+EXECUTE p2(2);
+ a | b
+---+-----
+ 2 | bbb
+ 2 | bcd
+ 2 | yyy
+(3 rows)
+
+EXPLAIN (costs off) EXECUTE p2(2);
+ QUERY PLAN
+-------------------------
+ Append
+ -> Seq Scan on t1
+ Filter: (a = 2)
+ -> Seq Scan on t2
+ Filter: (a = 2)
+ -> Seq Scan on t3
+ Filter: (a = 2)
+(7 rows)
+
+-- also, case when privilege switch from superuser
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXECUTE p2(2);
+ a | b
+---+-----
+ 2 | bbb
+ 2 | yyy
+(2 rows)
+
+EXPLAIN (costs off) EXECUTE p2(2);
+ QUERY PLAN
+---------------------------------------------------
+ Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a = 2) AND ((a % 2) = 0))
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a = 2) AND ((a % 2) = 1))
+ -> Seq Scan on t3
+ Filter: (a = 2)
+(9 rows)
+
+--
+-- UPDATE / DELETE and Row-level security
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXPLAIN (costs off) UPDATE t1 SET b = b || b WHERE f_leak(b);
+ QUERY PLAN
+-------------------------------------
+ Update on t1
+ -> Subquery Scan on t1_1
+ Filter: f_leak(t1_1.b)
+ -> Seq Scan on t1 t1_2
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ Filter: f_leak(t2.b)
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+ Filter: f_leak(b)
+(11 rows)
+
+UPDATE t1 SET b = b || b WHERE f_leak(b);
+NOTICE: f_leak => bbb
+NOTICE: f_leak => ddd
+NOTICE: f_leak => abc
+NOTICE: f_leak => cde
+NOTICE: f_leak => xxx
+NOTICE: f_leak => yyy
+NOTICE: f_leak => zzz
+EXPLAIN (costs off) UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b);
+ QUERY PLAN
+-------------------------------------
+ Update on t1
+ -> Subquery Scan on t1_1
+ Filter: f_leak(t1_1.b)
+ -> Seq Scan on t1 t1_2
+ Filter: ((a % 2) = 0)
+(5 rows)
+
+UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b);
+NOTICE: f_leak => bbbbbb
+NOTICE: f_leak => dddddd
+-- returning clause with system column
+UPDATE only t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+NOTICE: f_leak => bbbbbb_updt
+NOTICE: f_leak => dddddd_updt
+ oid | a | b | t1
+-----+---+-------------+-----------------
+ 102 | 2 | bbbbbb_updt | (2,bbbbbb_updt)
+ 104 | 4 | dddddd_updt | (4,dddddd_updt)
+(2 rows)
+
+UPDATE t1 SET b = b WHERE f_leak(b) RETURNING *;
+NOTICE: f_leak => bbbbbb_updt
+NOTICE: f_leak => dddddd_updt
+NOTICE: f_leak => abcabc
+NOTICE: f_leak => cdecde
+NOTICE: f_leak => xxxxxx
+NOTICE: f_leak => yyyyyy
+NOTICE: f_leak => zzzzzz
+ a | b
+---+-------------
+ 2 | bbbbbb_updt
+ 4 | dddddd_updt
+ 1 | abcabc
+ 3 | cdecde
+ 1 | xxxxxx
+ 2 | yyyyyy
+ 3 | zzzzzz
+(7 rows)
+
+UPDATE t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+NOTICE: f_leak => bbbbbb_updt
+NOTICE: f_leak => dddddd_updt
+NOTICE: f_leak => abcabc
+NOTICE: f_leak => cdecde
+NOTICE: f_leak => xxxxxx
+NOTICE: f_leak => yyyyyy
+NOTICE: f_leak => zzzzzz
+ oid | a | b | t1
+-----+---+-------------+-----------------
+ 102 | 2 | bbbbbb_updt | (2,bbbbbb_updt)
+ 104 | 4 | dddddd_updt | (4,dddddd_updt)
+ 201 | 1 | abcabc | (1,abcabc)
+ 203 | 3 | cdecde | (3,cdecde)
+ 301 | 1 | xxxxxx | (1,xxxxxx)
+ 302 | 2 | yyyyyy | (2,yyyyyy)
+ 303 | 3 | zzzzzz | (3,zzzzzz)
+(7 rows)
+
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1;
+ a | b
+---+-------------
+ 1 | aaa
+ 3 | ccc
+ 2 | bbbbbb_updt
+ 4 | dddddd_updt
+ 2 | bcd
+ 4 | def
+ 1 | abcabc
+ 3 | cdecde
+ 1 | xxxxxx
+ 2 | yyyyyy
+ 3 | zzzzzz
+(11 rows)
+
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXPLAIN (costs off) DELETE FROM only t1 WHERE f_leak(b);
+ QUERY PLAN
+-------------------------------------
+ Delete on t1
+ -> Subquery Scan on t1_1
+ Filter: f_leak(t1_1.b)
+ -> Seq Scan on t1 t1_2
+ Filter: ((a % 2) = 0)
+(5 rows)
+
+EXPLAIN (costs off) DELETE FROM t1 WHERE f_leak(b);
+ QUERY PLAN
+-------------------------------------
+ Delete on t1
+ -> Subquery Scan on t1_1
+ Filter: f_leak(t1_1.b)
+ -> Seq Scan on t1 t1_2
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ Filter: f_leak(t2.b)
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+ Filter: f_leak(b)
+(11 rows)
+
+DELETE FROM only t1 WHERE f_leak(b) RETURNING oid, *, t1;
+NOTICE: f_leak => bbbbbb_updt
+NOTICE: f_leak => dddddd_updt
+ oid | a | b | t1
+-----+---+-------------+-----------------
+ 102 | 2 | bbbbbb_updt | (2,bbbbbb_updt)
+ 104 | 4 | dddddd_updt | (4,dddddd_updt)
+(2 rows)
+
+DELETE FROM t1 WHERE f_leak(b) RETURNING oid, *, t1;
+NOTICE: f_leak => abcabc
+NOTICE: f_leak => cdecde
+NOTICE: f_leak => xxxxxx
+NOTICE: f_leak => yyyyyy
+NOTICE: f_leak => zzzzzz
+ oid | a | b | t1
+-----+---+--------+------------
+ 201 | 1 | abcabc | (1,abcabc)
+ 203 | 3 | cdecde | (3,cdecde)
+ 301 | 1 | xxxxxx | (1,xxxxxx)
+ 302 | 2 | yyyyyy | (2,yyyyyy)
+ 303 | 3 | zzzzzz | (3,zzzzzz)
+(5 rows)
+
+--
+-- Test psql \dt+ command
+--
+ALTER TABLE category RESET ROW SECURITY FOR ALL; -- too long qual
+\dt+
+ List of relations
+ Schema | Name | Type | Owner | Size | Description | Row-security
+--------------------+----------+-------+-------------------+------------+-------------+----------------------------------
+ rls_regress_schema | category | table | rls_regress_user0 | 16 kB | |
+ rls_regress_schema | document | table | rls_regress_user0 | 16 kB | | (dauthor = "current_user"())
+ rls_regress_schema | s1 | table | rls_regress_user0 | 16 kB | | (a IN ( SELECT v2.x +
+ | | | | | | FROM v2))
+ rls_regress_schema | s2 | table | rls_regress_user0 | 16 kB | | (x IN ( SELECT s1.a +
+ | | | | | | FROM s1 +
+ | | | | | | WHERE (s1.b ~~ '%d2%'::text)))
+ rls_regress_schema | t1 | table | rls_regress_user0 | 16 kB | | ((a % 2) = 0)
+ rls_regress_schema | t2 | table | rls_regress_user0 | 16 kB | | ((a % 2) = 1)
+ rls_regress_schema | t3 | table | rls_regress_user0 | 16 kB | |
+ rls_regress_schema | uaccount | table | rls_regress_user0 | 8192 bytes | |
+(8 rows)
+
+--
+-- Clean up objects
+--
+RESET SESSION AUTHORIZATION;
+DROP SCHEMA rls_regress_schema CASCADE;
+NOTICE: drop cascades to 10 other objects
+DETAIL: drop cascades to function f_leak(text)
+drop cascades to table uaccount
+drop cascades to table category
+drop cascades to table document
+drop cascades to table t1
+drop cascades to table t2
+drop cascades to table t3
+drop cascades to table s1
+drop cascades to table s2
+drop cascades to view v2
+DROP USER rls_regress_user0;
+DROP USER rls_regress_user1;
+DROP USER rls_regress_user2;
diff --git a/src/test/regress/expected/sanity_check.out b/src/test/regress/expected/sanity_check.out
index 432d39a..3b9123d 100644
--- a/src/test/regress/expected/sanity_check.out
+++ b/src/test/regress/expected/sanity_check.out
@@ -120,6 +120,7 @@ SELECT relname, relhasindex
pg_proc | t
pg_range | t
pg_rewrite | t
+ pg_rowsecurity | t
pg_seclabel | t
pg_shdepend | t
pg_shdescription | t
@@ -166,7 +167,7 @@ SELECT relname, relhasindex
timetz_tbl | f
tinterval_tbl | f
varchar_tbl | f
-(155 rows)
+(156 rows)
--
-- another sanity check: every system catalog that has OIDs should have
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 3e6b306..f2d75f5 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 matview
+test: privileges rowsecurity security_label collate matview
# ----------
# Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 3ad289f..d44e89b 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -93,6 +93,7 @@ test: delete
test: namespace
test: prepared_xacts
test: privileges
+test: rowsecurity
test: security_label
test: collate
test: matview
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
new file mode 100644
index 0000000..55d4aad
--- /dev/null
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -0,0 +1,298 @@
+--
+-- Test of Row-level security feature
+--
+
+-- Clean up in case a prior regression run failed
+
+-- Suppress NOTICE messages when users/groups don't exist
+SET client_min_messages TO 'warning';
+
+DROP USER IF EXISTS rls_regress_user0;
+DROP USER IF EXISTS rls_regress_user1;
+DROP USER IF EXISTS rls_regress_user2;
+
+DROP SCHEMA IF EXISTS rls_regress_schema CASCADE;
+
+RESET client_min_messages;
+
+-- initial setup
+CREATE USER rls_regress_user0;
+CREATE USER rls_regress_user1;
+CREATE USER rls_regress_user2;
+
+CREATE SCHEMA rls_regress_schema;
+GRANT ALL ON SCHEMA rls_regress_schema TO public;
+SET search_path = rls_regress_schema;
+
+-- setup of malicious function
+CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool
+ COST 0.0000001 LANGUAGE plpgsql
+ AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END';
+GRANT EXECUTE ON FUNCTION f_leak(text) TO public;
+
+-- BASIC Row-Level Security Scenario
+
+SET SESSION AUTHORIZATION rls_regress_user0;
+CREATE TABLE uaccount (
+ pguser name primary key,
+ seclv int
+);
+INSERT INTO uaccount VALUES
+ ('rls_regress_user0', 99),
+ ('rls_regress_user1', 1),
+ ('rls_regress_user2', 2),
+ ('rls_regress_user3', 3);
+GRANT SELECT ON uaccount TO public;
+
+CREATE TABLE category (
+ cid int primary key,
+ cname text
+);
+GRANT ALL ON category TO public;
+INSERT INTO category VALUES
+ (11, 'novel'),
+ (22, 'science fiction'),
+ (33, 'technology'),
+ (44, 'manga');
+
+CREATE TABLE document (
+ did int primary key,
+ cid int references category(cid),
+ dlevel int not null,
+ dauthor name,
+ dtitle text
+);
+GRANT ALL ON document TO public;
+INSERT INTO document VALUES
+ ( 1, 11, 1, 'rls_regress_user1', 'my first novel'),
+ ( 2, 11, 2, 'rls_regress_user1', 'my second novel'),
+ ( 3, 22, 2, 'rls_regress_user1', 'my science fiction'),
+ ( 4, 44, 1, 'rls_regress_user1', 'my first manga'),
+ ( 5, 44, 2, 'rls_regress_user1', 'my second manga'),
+ ( 6, 22, 1, 'rls_regress_user2', 'great science fiction'),
+ ( 7, 33, 2, 'rls_regress_user2', 'great technology book'),
+ ( 8, 44, 1, 'rls_regress_user2', 'great manga');
+
+-- user's security level must higher than or equal to document's one
+ALTER TABLE document SET ROW SECURITY FOR ALL
+ TO (dlevel <= (SELECT seclv FROM uaccount WHERE pguser = current_user));
+
+-- viewpoint from rls_regress_user1
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+-- viewpoint from rls_regress_user2
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+-- only owner can change row-level security
+ALTER TABLE document SET ROW SECURITY FOR ALL TO (true); -- fail
+ALTER TABLE document RESET ROW SECURITY FOR ALL; -- fail
+
+SET SESSION AUTHORIZATION rls_regress_user0;
+ALTER TABLE document SET ROW SECURITY FOR ALL TO (dauthor = current_user);
+
+-- viewpoint from rls_regress_user1 again
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+-- viewpoint from rls_regress_user2 again
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+-- interaction of FK/PK constraints
+SET SESSION AUTHORIZATION rls_regress_user0;
+ALTER TABLE category SET ROW SECURITY FOR ALL
+ TO (CASE WHEN current_user = 'rls_regress_user1' THEN cid IN (11, 33)
+ WHEN current_user = 'rls_regress_user2' THEN cid IN (22, 44)
+ ELSE false END);
+
+-- cannot delete PK referenced by invisible FK
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document d full outer join category c on d.cid = c.cid;
+DELETE FROM category WHERE cid = 33; -- failed
+
+-- cannot insert FK referencing invisible PK
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document d full outer join category c on d.cid = c.cid;
+INSERT INTO document VALUES (10, 33, 1, current_user, 'hoge'); -- failed
+
+-- database superuser can bypass RLS policy
+RESET SESSION AUTHORIZATION;
+SELECT * FROM document;
+SELECT * FROM category;
+
+--
+-- Table inheritance and RLS policy
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+
+CREATE TABLE t1 (a int, junk1 text, b text) WITH OIDS;
+ALTER TABLE t1 DROP COLUMN junk1; -- just a disturbing factor
+GRANT ALL ON t1 TO public;
+
+COPY t1 FROM stdin WITH (oids);
+101 1 aaa
+102 2 bbb
+103 3 ccc
+104 4 ddd
+\.
+
+CREATE TABLE t2 (c float) INHERITS (t1);
+COPY t2 FROM stdin WITH (oids);
+201 1 abc 1.1
+202 2 bcd 2.2
+203 3 cde 3.3
+204 4 def 4.4
+\.
+
+CREATE TABLE t3 (c text, b text, a int) WITH OIDS;
+ALTER TABLE t3 INHERIT t1;
+COPY t3(a,b,c) FROM stdin WITH (oids);
+301 1 xxx X
+302 2 yyy Y
+303 3 zzz Z
+\.
+
+ALTER TABLE t1 SET ROW SECURITY FOR ALL TO (a % 2 = 0); -- be even number
+ALTER TABLE t2 SET ROW SECURITY FOR ALL TO (a % 2 = 1); -- be odd number
+
+SELECT * FROM t1;
+EXPLAIN (costs off) SELECT * FROM t1;
+
+SELECT * FROM t1 WHERE f_leak(b);
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+
+-- reference to system column
+SELECT oid, * FROM t1;
+EXPLAIN (costs off) SELECT * FROM t1;
+
+-- reference to whole-row reference
+SELECT *,t1 FROM t1;
+EXPLAIN (costs off) SELECT *,t1 FROM t1;
+
+-- for share/update lock
+SELECT * FROM t1 FOR SHARE;
+EXPLAIN (costs off) SELECT * FROM t1 FOR SHARE;
+
+SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+
+--
+-- COPY TO statement
+--
+COPY t1 TO stdout;
+COPY t1 TO stdout WITH OIDS;
+COPY t2(c,b) TO stdout WITH OIDS;
+COPY (SELECT * FROM t1) TO stdout;
+COPY document TO stdout WITH OIDS; -- failed (no oid column)
+
+--
+-- recursive RLS and VIEWs in policy
+--
+CREATE TABLE s1 (a int, b text);
+INSERT INTO s1 (SELECT x, md5(x::text) FROM generate_series(-10,10) x);
+
+CREATE TABLE s2 (x int, y text);
+INSERT INTO s2 (SELECT x, md5(x::text) FROM generate_series(-6,6) x);
+CREATE VIEW v2 AS SELECT * FROM s2 WHERE y like '%af%';
+
+ALTER TABLE s1 SET ROW SECURITY FOR ALL
+ TO (a in (select x from s2 where y like '%2f%'));
+
+ALTER TABLE s2 SET ROW SECURITY FOR ALL
+ TO (x in (select a from s1 where b like '%22%'));
+
+SELECT * FROM s1 WHERE f_leak(b); -- fail (infinite recursion)
+
+ALTER TABLE s2 SET ROW SECURITY FOR ALL TO (x % 2 = 0);
+
+SELECT * FROM s1 WHERE f_leak(b); -- OK
+EXPLAIN SELECT * FROM only s1 WHERE f_leak(b);
+
+ALTER TABLE s1 SET ROW SECURITY FOR ALL
+ TO (a in (select x from v2)); -- using VIEW in RLS policy
+SELECT * FROM s1 WHERE f_leak(b); -- OK
+EXPLAIN (COSTS OFF) SELECT * FROM s1 WHERE f_leak(b);
+
+SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+EXPLAIN (COSTS OFF) SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+
+ALTER TABLE s2 SET ROW SECURITY FOR ALL
+ TO (x in (select a from s1 where b like '%d2%'));
+SELECT * FROM s1 WHERE f_leak(b); -- fail (infinite recursion via view)
+
+-- prepared statement with rls_regress_user0 privilege
+PREPARE p1(int) AS SELECT * FROM t1 WHERE a <= $1;
+EXECUTE p1(2);
+EXPLAIN (costs off) EXECUTE p1(2);
+
+-- superuser is allowed to bypass RLS checks
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1 WHERE f_leak(b);
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+
+-- plan cache should be invalidated
+EXECUTE p1(2);
+EXPLAIN (costs off) EXECUTE p1(2);
+
+PREPARE p2(int) AS SELECT * FROM t1 WHERE a = $1;
+EXECUTE p2(2);
+EXPLAIN (costs off) EXECUTE p2(2);
+
+-- also, case when privilege switch from superuser
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXECUTE p2(2);
+EXPLAIN (costs off) EXECUTE p2(2);
+
+--
+-- UPDATE / DELETE and Row-level security
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXPLAIN (costs off) UPDATE t1 SET b = b || b WHERE f_leak(b);
+UPDATE t1 SET b = b || b WHERE f_leak(b);
+
+EXPLAIN (costs off) UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b);
+UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b);
+
+-- returning clause with system column
+UPDATE only t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+UPDATE t1 SET b = b WHERE f_leak(b) RETURNING *;
+UPDATE t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1;
+
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXPLAIN (costs off) DELETE FROM only t1 WHERE f_leak(b);
+EXPLAIN (costs off) DELETE FROM t1 WHERE f_leak(b);
+
+DELETE FROM only t1 WHERE f_leak(b) RETURNING oid, *, t1;
+DELETE FROM t1 WHERE f_leak(b) RETURNING oid, *, t1;
+
+--
+-- Test psql \dt+ command
+--
+ALTER TABLE category RESET ROW SECURITY FOR ALL; -- too long qual
+\dt+
+
+--
+-- Clean up objects
+--
+RESET SESSION AUTHORIZATION;
+
+DROP SCHEMA rls_regress_schema CASCADE;
+
+DROP USER rls_regress_user0;
+DROP USER rls_regress_user1;
+DROP USER rls_regress_user2;
Sorry, this included a garbage chunk when I made a patch.
The attached one is corrected revision. Please see this patch instead.
Thanks,
2013/7/8 Kohei KaiGai <kaigai@kaigai.gr.jp>:
I was pointed out that the previous patch was not applied cleanly because of
enhancement on pg_class system catalog, and related pg_dump portion was
getting broken.The attached patch fixes this matters. Please reference this patch instead.
Thanks,2013/6/13 Kohei KaiGai <kaigai@kaigai.gr.jp>:
The attached patch implements row-level security feature; that allows to
enforce a pre-configured security policy on reference of tables with the
row-level security policy.
It enables to isolate records to be visible from others according to access
control decision, usually done based on current user's credential.
It will make sense to ensure correctness of security for SaaS style
applications that typically share a common table for multiple users but
correctness of access control was ensured with correctness of application
itself.Here is not functional update since the last commit fest for v9.3 except
for adjustment towards the latest master branch.So, the explanation below might be bored for someone.
This feature enhances ALTER TABLE statement as follows:
ALTER TABLE <tablename> SET ROW SECURITY
FOR <command> TO (<expression>);
ALTER TABLE <tablename> RESET ROW SECURITY
FOR <command>;
<command> := { ALL | SELECT | INSERT | UPDATE | DELETE }Right now, only "ALL" is supported command, even though syntax reserves
future enhancement allows to set individual security policy for each command.
The <expression> should be an expression that returns a bool value. It can
reference any column in the target table and contain sub-query that reference
another tables.
Then, the pre-configured expression shall be added when the table is referenced.See below, it gives "(X % 2 = 1)" as security policy, user can see the record
that has odd-number at X. The EXPLAIN output below shows this expression
was automatically attached.postgres=> ALTER TABLE tbl SET ROW SECURITY FOR ALL TO (x % 2 = 1);
ALTER TABLE
postgres=> EXPLAIN SELECT * FROM tbl WHERE y like '%abc%';
QUERY PLAN
-----------------------------------------------------------------
Subquery Scan on tbl (cost=0.00..28.52 rows=1 width=36)
Filter: (tbl.y ~~ '%abc%'::text)
-> Seq Scan on tbl tbl_1 (cost=0.00..28.45 rows=6 width=36)
Filter: ((x % 2) = 1)
(4 rows)An important point is, reference to a particular relation is replaced
with a sub-
query that has security policy expression and security barrier attribute.
That prevent any (non leakproof) user given condition earlier than
security poliy
itself, thus, it ensures all records user can see is satisfies the
security policy.On writer-queries, things to do are similar. It adds security policy expression
on the scan phase of UPDATE or DELETE statement. Thus, only visible records
are updatable or deletable.postgres=> EXPLAIN UPDATE tbl SET y = y || '_update' WHERE y like '%xyz%';
QUERY PLAN
-----------------------------------------------------------------------
Update on tbl (cost=0.00..28.53 rows=1 width=42)
-> Subquery Scan on tbl_1 (cost=0.00..28.53 rows=1 width=42)
Filter: (tbl_1.y ~~ '%xyz%'::text)
-> Seq Scan on tbl tbl_2 (cost=0.00..28.45 rows=6 width=42)
Filter: ((x % 2) = 1)
(5 rows)I had a relevant presentation at PGcon last month. I think its slides
are good summary
to know brief background of the long-standing problem.
http://www.pgcon.org/2013/schedule/attachments/273_PGcon2013-kaigai-row-level-security.pdfThanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>--
KaiGai Kohei <kaigai@kaigai.gr.jp>
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
Attachments:
pgsql-v9.4-row-level-security.v3.patchapplication/octet-stream; name=pgsql-v9.4-row-level-security.v3.patchDownload
doc/src/sgml/catalogs.sgml | 65 ++
doc/src/sgml/ref/alter_table.sgml | 43 ++
doc/src/sgml/user-manag.sgml | 145 +++++
src/backend/catalog/Makefile | 4 +-
src/backend/catalog/dependency.c | 8 +
src/backend/catalog/heap.c | 1 +
src/backend/catalog/objectaddress.c | 50 ++
src/backend/catalog/pg_rowsecurity.c | 337 ++++++++++
src/backend/commands/copy.c | 90 ++-
src/backend/commands/event_trigger.c | 1 +
src/backend/commands/tablecmds.c | 27 +
src/backend/nodes/copyfuncs.c | 3 +
src/backend/nodes/equalfuncs.c | 3 +
src/backend/nodes/nodeFuncs.c | 12 +-
src/backend/nodes/outfuncs.c | 3 +
src/backend/nodes/readfuncs.c | 2 +
src/backend/optimizer/plan/planner.c | 23 +-
src/backend/optimizer/prep/preptlist.c | 61 +-
src/backend/optimizer/prep/prepunion.c | 95 ++-
src/backend/optimizer/util/Makefile | 2 +-
src/backend/optimizer/util/rowsecurity.c | 733 ++++++++++++++++++++++
src/backend/parser/gram.y | 25 +
src/backend/parser/parse_agg.c | 6 +
src/backend/parser/parse_expr.c | 3 +
src/backend/parser/parse_relation.c | 19 +
src/backend/rewrite/rewriteHandler.c | 16 +
src/backend/utils/adt/ri_triggers.c | 13 +-
src/backend/utils/cache/plancache.c | 32 +
src/backend/utils/cache/relcache.c | 17 +-
src/bin/pg_dump/common.c | 4 +
src/bin/pg_dump/pg_backup_archiver.c | 1 +
src/bin/pg_dump/pg_dump.c | 186 +++++-
src/bin/pg_dump/pg_dump.h | 13 +-
src/bin/pg_dump/pg_dump_sort.c | 5 +
src/bin/psql/describe.c | 7 +
src/include/catalog/dependency.h | 1 +
src/include/catalog/indexing.h | 5 +
src/include/catalog/pg_class.h | 24 +-
src/include/catalog/pg_rowsecurity.h | 76 +++
src/include/miscadmin.h | 1 +
src/include/nodes/execnodes.h | 4 +
src/include/nodes/nodeFuncs.h | 1 +
src/include/nodes/parsenodes.h | 14 +-
src/include/nodes/plannodes.h | 2 +
src/include/nodes/relation.h | 6 +
src/include/optimizer/rowsecurity.h | 27 +
src/include/parser/parse_node.h | 3 +-
src/include/parser/parsetree.h | 5 +-
src/include/rewrite/rewriteHandler.h | 1 +
src/include/utils/plancache.h | 2 +
src/include/utils/rel.h | 2 +
src/test/regress/expected/rowsecurity.out | 950 +++++++++++++++++++++++++++++
src/test/regress/expected/sanity_check.out | 3 +-
src/test/regress/parallel_schedule | 2 +-
src/test/regress/serial_schedule | 1 +
src/test/regress/sql/rowsecurity.sql | 298 +++++++++
56 files changed, 3440 insertions(+), 43 deletions(-)
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 6715782..6f3c8fc 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -234,6 +234,11 @@
</row>
<row>
+ <entry><link linkend="catalog-pg-rowlevelsec"><structname>pg_rowlevelsec</structname></link></entry>
+ <entry>row-level security policy of relation</entry>
+ </row>
+
+ <row>
<entry><link linkend="catalog-pg-seclabel"><structname>pg_seclabel</structname></link></entry>
<entry>security labels on database objects</entry>
</row>
@@ -1848,6 +1853,16 @@
</row>
<row>
+ <entry><structfield>relhasrowsecurity</structfield></entry>
+ <entry><type>bool</type></entry>
+ <entry></entry>
+ <entry>
+ True if table has row-security policy; see
+ <link linkend="catalog-pg-rowsecurity"><structname>pg_rowsecurity</structname></link> catalog
+ </entry>
+ </row>
+
+ <row>
<entry><structfield>relhassubclass</structfield></entry>
<entry><type>bool</type></entry>
<entry></entry>
@@ -5112,6 +5127,56 @@
</sect1>
+ <sect1 id="catalog-pg-rowsecurity">
+ <title><structname>pg_rowlevelsec</structname></title>
+
+ <indexterm zone="catalog-pg-rowsecurity">
+ <primary>pg_rowsecurity</primary>
+ </indexterm>
+ <para>
+ The catalog <structname>pg_rowsecurity</structname> stores expression
+ tree of row-security policy to be performed on a particular relation.
+ </para>
+ <table>
+ <title><structname>pg_rowsecurity</structname> Columns</title>
+ <tgroup cols="4">
+ <thead>
+ <row>
+ <entry>Name</entry>
+ <entry>Type</entry>
+ <entry>References</entry>
+ <entry>Description</entry>
+ </row>
+ </thead>
+ <tbody>
+ <row>
+ <entry><structfield>rsecrelid</structfield></entry>
+ <entry><type>oid</type></entry>
+ <entry><literal><link linkend="catalog-pg-class"><structname>pg_class</structname></link>.oid</literal></entry>
+ <entry>The table this row-security is for</entry>
+ </row>
+ <row>
+ <entry><structfield>rseccmd</structfield></entry>
+ <entry><type>char</type></entry>
+ <entry></entry>
+ <entry>The command this row-security is for. 'a' meaning all is only possible value right now.</entry>
+ </row>
+ <row>
+ <entry><structfield>rsecqual</structfield></entry>
+ <entry><type>pg_node_tree</type></entry>
+ <entry></entry>
+ <entry>An expression tree to be performed as rowl-security policy</entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+ <note>
+ <para>
+ <literal>pg_class.relhasrowlevelsec</literal>
+ must be true if a table has row-level security policy in this catalog.
+ </para>
+ </note>
+ </sect1>
<sect1 id="catalog-pg-seclabel">
<title><structname>pg_seclabel</structname></title>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 2609d4a..b380852 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -69,12 +69,16 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable>
NOT OF
OWNER TO <replaceable class="PARAMETER">new_owner</replaceable>
SET TABLESPACE <replaceable class="PARAMETER">new_tablespace</replaceable>
+ SET ROW SECURITY FOR <replaceable class="PARAMETER">rowsec_command</replaceable> TO (<replaceable class="PARAMETER">condition</replaceable>)
+ RESET ROW SECURITY FOR <replaceable class="PARAMETER">rowsec_command</replaceable>
<phrase>and <replaceable class="PARAMETER">table_constraint_using_index</replaceable> is:</phrase>
[ CONSTRAINT <replaceable class="PARAMETER">constraint_name</replaceable> ]
{ UNIQUE | PRIMARY KEY } USING INDEX <replaceable class="PARAMETER">index_name</replaceable>
[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+<phrase>and <replaceable class="PARAMETER">rowsec_command</replaceable> is:</phrase>
+ { ALL | SELECT | INSERT | UPDATE | DELETE }
</synopsis>
</refsynopsisdiv>
@@ -580,6 +584,31 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable>
</varlistentry>
<varlistentry>
+ <term><literal>SET ROW SECURITY FOR <replaceable class="PARAMETER">rowsec_command</replaceable> TO (<replaceable class="PARAMETER">condition</replaceable>)</literal></term>
+ <listitem>
+ <para>
+ This form set row-level security policy of the table.
+ Supplied <replaceable class="PARAMETER">condition</replaceable> performs
+ as if it is implicitly appended to the qualifiers of <literal>WHERE</literal>
+ clause, although mechanism guarantees to evaluate this condition earlier
+ than any other user given condition.
+ <literal>ALL</> is the only supported command type right now.
+ See also <xref linkend="ROW-SECURITY">.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>RESET ROW SECURITY FOR <replaceable class="PARAMETER">rowsec_command</replaceable></literal></term>
+ <listitem>
+ <para>
+ This form reset row-level security policy of the table, if exists.
+ <literal>ALL</> is the only supported command type right now.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
<term><literal>RENAME</literal></term>
<listitem>
<para>
@@ -821,6 +850,20 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable>
</listitem>
</varlistentry>
+ <varlistentry>
+ <term><replaceable class="PARAMETER">condition</replaceable></term>
+ <listitem>
+ <para>
+ An expression that returns a value of type boolean. Expect for a case
+ when queries are executed with superuser privilege, only rows for which
+ this expression returns true will be fetched, updated or deleted.
+ This expression can reference columns of the relation being configured.
+ Sub-queries can be contained within expression tree, unless referenced
+ relation recursively references the same relation.
+ </para>
+ </listitem>
+ </varlistentry>
+
</variablelist>
</refsect1>
diff --git a/doc/src/sgml/user-manag.sgml b/doc/src/sgml/user-manag.sgml
index 177ac7a..34a699e 100644
--- a/doc/src/sgml/user-manag.sgml
+++ b/doc/src/sgml/user-manag.sgml
@@ -439,4 +439,149 @@ DROP ROLE <replaceable>name</replaceable>;
</para>
</sect1>
+ <sect1 id="row-security">
+ <title>Row-security</title>
+ <para>
+ <productname>PostgreSQL</productname> v9.3 or later provides
+ row-security feature, like several commercial database
+ management system. It allows table owner to assign a particular
+ condition that performs as a security policy of the table; only
+ rows that satisfies the condition should be visible, except for
+ a case when superuser runs queries.
+ </para>
+ <para>
+ Row-security policy can be set using <literal>SET ROW SECURITY</literal>
+ command of <xref linkend="SQL-ALTERTABLE"> 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.
+ </para>
+ <para>
+ For example, the following <literal>customer</literal> table has
+ <literal>uname</literal> field to store user name, and it assume
+ we don't want to expose any properties of other customers.
+ The following command set <literal>current_user = uname</literal>
+ as row-security policy on the <literal>customer</literal> table.
+<screen>
+postgres=> ALTER TABLE customer SET ROW SECURITY
+ FOR ALL TO (current_user = uname);
+ALTER TABLE
+</screen>
+ <xref linkend="SQL-EXPLAIN"> command shows how row-security policy
+ works on the supplied query.
+<screen>
+postgres=> EXPLAIN(costs off) SELECT * FROM customer WHERE f_leak(upasswd);
+ QUERY PLAN
+--------------------------------------------
+ Subquery Scan on customer
+ Filter: f_leak(customer.upasswd)
+ -> Seq Scan on customer customer_1
+ Filter: ("current_user"() = uname)
+(4 rows)
+</screen>
+ This query execution plan means the preconfigured row-security policy is
+ implicitly added, and scan plan on the target relation being wrapped up
+ with a sub-query.
+ It ensures user given qualifiers, including functions with side effects,
+ are never executed earlier than the row-security policy regardless of
+ its cost, except for the cases when these were fully leakproof.
+ This design helps to tackle the scenario described in
+ <xref linkend="RULES-PRIVILEGES">; that introduces the order to evaluate
+ qualifiers is significant to keep confidentiality of invisible rows.
+ </para>
+
+ <para>
+ On the other hand, this design allows superusers to bypass checks with
+ row-security.
+ It ensures <application>pg_dump</application> can obtain a complete set
+ of database backup, and avoid to execute Trojan horse trap, being injected
+ as a row-security policy of user-defined table, with privileges of
+ superuser.
+ </para>
+
+ <para>
+ In case of queries on inherited tables, row-security policy of the parent
+ relation is not applied to child relations. Scope of the row-security
+ policy is limited to the relation on which it is set.
+<screen>
+postgres=> EXPLAIN(costs off) SELECT * FROM t1 WHERE f_leak(y);
+ QUERY PLAN
+-------------------------------------
+ Append
+ -> Subquery Scan on t1
+ Filter: f_leak(t1.y)
+ -> Seq Scan on t1 t1_1
+ Filter: ((x % 2) = 0)
+ -> Seq Scan on t2
+ Filter: f_leak(y)
+ -> Subquery Scan on t3
+ Filter: f_leak(t3.y)
+ -> Seq Scan on t3 t3_1
+ Filter: ((x % 2) = 1)
+(11 rows)
+</screen>
+ In the above example, <literal>t1</literal> has inherited
+ child table <literal>t2</literal> and <literal>t3</literal>,
+ and row-security policy is set on only <literal>t1</literal>,
+ and <literal>t3</literal>, not <literal>t2</literal>.
+
+ The row-security policy of <literal>t1</literal>, <literal>x</literal>
+ must be even-number, is appended only <literal>t1</literal>,
+ neither <literal>t2</literal> nor <literal>t3</literal>.
+ On the contrary, <literal>t3</literal> has different row-security policy;
+ <literal>x</literal> must be odd-number.
+ </para>
+
+ <para>
+ Row-security feature also works to queries for writer-operations; such as
+ <xref linkend="SQL-INSERT">, <xref linkend="SQL-UPDATE"> or
+ <xref linkend="SQL-DELETE"> commands.
+ It ensures all the modified rows satisfies configured row-security policy.
+ The below query tries to update e-mail address of the
+ <literal>customer</> table, and configured row-security makes sure all the
+ modified rows's <literal>uname</> field has to match with
+ <literal>current_user</>.
+ </para>
+<screen>
+postgres=> EXPLAIN (costs off) UPDATE customer SET email = 'alice@example.com';
+ QUERY PLAN
+--------------------------------------------------
+ Update on customer
+ -> Subquery Scan on customer
+ -> Seq Scan on customer customer_1
+ Filter: ("current_user"() = uname)
+(4 rows)
+</screen>
+ <para>
+ Modification of a certain table is consist of two different stuffs; one
+ is fetch rows to be modified from the result relation (except for
+ <literal>INSERT</> command), second is insertion of rows to the
+ result relation. Usually, before-row trigger or check constraints are
+ run in the second phase. In addition, row-security policy is also checked
+ on this stage, to prevent to insert or update rows with unprivileged
+ values.
+ </para>
+
+ <para>
+ Unlike other commercial database systems, we don't have any plan to allow
+ individual row-security policy for each command type. Even if we want to
+ perform with difference policy between <xref linkend="SQL-SELECT"> and
+ <xref linkend="SQL-DELETE">, <literal>RETURNING</> clause can leak the rows
+ to be invisible using <xref linkend="SQL-DELETE"> command.
+ </para>
+
+ <para>
+ Even though it is not a specific matter in row-security, please be careful
+ if you plan to use <literal>current_user</> in row-level security policy.
+ <xref linkend="SQL-DECLARE"> and <xref linkend="SQL-FETCH"> allows to
+ switch current user identifier during execution of the query.
+ Thus, it can fetch the first 100 rows with privilege of <literal>alice</>,
+ then remaining rows with privilege of <literal>bob</>. If and when query
+ execution plan contains some kind of materialization and row-security
+ policy contains <literal>current_user</>, the fetched tuples in
+ <literal>bob</>'s screen might be evaluated according to the privilege of
+ <literal>alice</>.
+ </para>
+ </sect1>
</chapter>
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index c4d3f3c..9b4e9f5 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -15,7 +15,7 @@ OBJS = catalog.o dependency.o heap.o index.o indexing.o namespace.o aclchk.o \
pg_constraint.o pg_conversion.o \
pg_depend.o pg_enum.o pg_inherits.o pg_largeobject.o pg_namespace.o \
pg_operator.o pg_proc.o pg_range.o pg_db_role_setting.o pg_shdepend.o \
- pg_type.o storage.o toasting.o
+ pg_rowsecurity.o pg_type.o storage.o toasting.o
BKIFILES = postgres.bki postgres.description postgres.shdescription
@@ -39,7 +39,7 @@ POSTGRES_BKI_SRCS = $(addprefix $(top_srcdir)/src/include/catalog/,\
pg_ts_config.h pg_ts_config_map.h pg_ts_dict.h \
pg_ts_parser.h pg_ts_template.h pg_extension.h \
pg_foreign_data_wrapper.h pg_foreign_server.h pg_user_mapping.h \
- pg_foreign_table.h \
+ pg_foreign_table.h pg_rowsecurity.h \
pg_default_acl.h pg_seclabel.h pg_shseclabel.h pg_collation.h pg_range.h \
toasting.h indexing.h \
)
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index fe17c96..d13b2e1 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_rowsecurity.h"
#include "catalog/pg_tablespace.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_ts_config.h"
@@ -1249,6 +1250,10 @@ doDeletion(const ObjectAddress *object, int flags)
RemoveEventTriggerById(object->objectId);
break;
+ case OCLASS_ROWSECURITY:
+ RemoveRowSecurityById(object->objectId);
+ break;
+
default:
elog(ERROR, "unrecognized object class: %u",
object->classId);
@@ -2308,6 +2313,9 @@ getObjectClass(const ObjectAddress *object)
case EventTriggerRelationId:
return OCLASS_EVENT_TRIGGER;
+
+ case RowSecurityRelationId:
+ return OCLASS_ROWSECURITY;
}
/* shouldn't get here */
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 64ca312..7eb7a17 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -791,6 +791,7 @@ InsertPgClassTuple(Relation pg_class_desc,
values[Anum_pg_class_relhaspkey - 1] = BoolGetDatum(rd_rel->relhaspkey);
values[Anum_pg_class_relhasrules - 1] = BoolGetDatum(rd_rel->relhasrules);
values[Anum_pg_class_relhastriggers - 1] = BoolGetDatum(rd_rel->relhastriggers);
+ values[Anum_pg_class_relhasrowsecurity - 1] = BoolGetDatum(rd_rel->relhasrowsecurity);
values[Anum_pg_class_relhassubclass - 1] = BoolGetDatum(rd_rel->relhassubclass);
values[Anum_pg_class_relispopulated - 1] = BoolGetDatum(rd_rel->relispopulated);
values[Anum_pg_class_relfrozenxid - 1] = TransactionIdGetDatum(rd_rel->relfrozenxid);
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 4d22f3a..b4bc9d8 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -2143,6 +2143,56 @@ getObjectDescription(const ObjectAddress *object)
break;
}
+ case OCLASS_ROWSECURITY:
+ {
+ Relation rsec_rel;
+ ScanKeyData skey;
+ SysScanDesc sscan;
+ HeapTuple tuple;
+ Form_pg_rowsecurity form_rsec;
+
+ rsec_rel = heap_open(RowSecurityRelationId, AccessShareLock);
+
+ ScanKeyInit(&skey,
+ ObjectIdAttributeNumber,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(object->objectId));
+ sscan = systable_beginscan(rsec_rel, RowSecurityOidIndexId,
+ true, SnapshotNow, 1, &skey);
+ tuple = systable_getnext(sscan);
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "could not find tuple for row-security %u",
+ object->objectId);
+ form_rsec = (Form_pg_rowsecurity) GETSTRUCT(tuple);
+
+ appendStringInfo(&buffer, _("row-security of "));
+ getRelationDescription(&buffer, form_rsec->rsecrelid);
+ switch (form_rsec->rseccmd)
+ {
+ case ROWSECURITY_CMD_ALL:
+ appendStringInfo(&buffer, _(" FOR ALL"));
+ break;
+ case ROWSECURITY_CMD_SELECT:
+ appendStringInfo(&buffer, _(" FOR SELECT"));
+ break;
+ case ROWSECURITY_CMD_INSERT:
+ appendStringInfo(&buffer, _(" FOR INSERT"));
+ break;
+ case ROWSECURITY_CMD_UPDATE:
+ appendStringInfo(&buffer, _(" FOR UPDATE"));
+ break;
+ case ROWSECURITY_CMD_DELETE:
+ appendStringInfo(&buffer, _(" FOR DELETE"));
+ break;
+ default:
+ elog(ERROR, "unexpected row-security command type: %c",
+ form_rsec->rseccmd);
+ }
+ systable_endscan(sscan);
+ heap_close(rsec_rel, AccessShareLock);
+ break;
+ }
+
default:
appendStringInfo(&buffer, "unrecognized object %u %u %d",
object->classId,
diff --git a/src/backend/catalog/pg_rowsecurity.c b/src/backend/catalog/pg_rowsecurity.c
new file mode 100644
index 0000000..25739ff
--- /dev/null
+++ b/src/backend/catalog/pg_rowsecurity.c
@@ -0,0 +1,337 @@
+/* -------------------------------------------------------------------------
+ *
+ * pg_rowsecurity.c
+ * routines to support manipulation of the pg_rowsecurity catalog
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * -------------------------------------------------------------------------
+ */
+#include "postgres.h"
+#include "access/genam.h"
+#include "access/heapam.h"
+#include "access/htup_details.h"
+#include "access/sysattr.h"
+#include "catalog/dependency.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_rowsecurity.h"
+#include "catalog/pg_type.h"
+#include "nodes/nodeFuncs.h"
+#include "optimizer/clauses.h"
+#include "parser/parse_clause.h"
+#include "parser/parse_node.h"
+#include "parser/parse_relation.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/inval.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/tqual.h"
+
+/*
+ * Load row-security policy from the catalog, and keep it on
+ * the relation cache.
+ */
+void
+RelationBuildRowSecurity(Relation relation)
+{
+ Relation catalog;
+ ScanKeyData skey;
+ SysScanDesc sscan;
+ HeapTuple tuple;
+ MemoryContext oldcxt;
+ MemoryContext rscxt = NULL;
+ RowSecurityDesc *rsdesc = NULL;
+
+ catalog = heap_open(RowSecurityRelationId, AccessShareLock);
+
+ ScanKeyInit(&skey,
+ Anum_pg_rowsecurity_rsecrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(RelationGetRelid(relation)));
+ sscan = systable_beginscan(catalog, RowSecurityRelidIndexId, true,
+ SnapshotNow, 1, &skey);
+ PG_TRY();
+ {
+ while (HeapTupleIsValid(tuple = systable_getnext(sscan)))
+ {
+ Datum value;
+ bool isnull;
+ char *temp;
+
+ if (!rsdesc)
+ {
+ rscxt = AllocSetContextCreate(CacheMemoryContext,
+ "Row-security descriptor",
+ ALLOCSET_SMALL_MINSIZE,
+ ALLOCSET_SMALL_INITSIZE,
+ ALLOCSET_SMALL_MAXSIZE);
+ oldcxt = MemoryContextSwitchTo(rscxt);
+ rsdesc = palloc0(sizeof(RowSecurityDesc));
+ rsdesc->rscxt = rscxt;
+ MemoryContextSwitchTo(oldcxt);
+ }
+ value = heap_getattr(tuple, Anum_pg_rowsecurity_rseccmd,
+ RelationGetDescr(catalog), &isnull);
+ Assert(!isnull);
+
+ if (DatumGetChar(value) != ROWSECURITY_CMD_ALL)
+ {
+ ereport(WARNING,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("Per-command row-security not implemented")));
+ continue;
+ }
+
+ value = heap_getattr(tuple, Anum_pg_rowsecurity_rsecqual,
+ RelationGetDescr(catalog), &isnull);
+ Assert(!isnull);
+ temp = TextDatumGetCString(value);
+
+ oldcxt = MemoryContextSwitchTo(rscxt);
+ rsdesc->rsall.rsecid = HeapTupleGetOid(tuple);
+ rsdesc->rsall.qual = (Expr *) stringToNode(temp);
+ Assert(exprType((Node *)rsdesc->rsall.qual) == BOOLOID);
+ rsdesc->rsall.hassublinks
+ = contain_subplans((Node *)rsdesc->rsall.qual);
+ MemoryContextSwitchTo(oldcxt);
+
+ pfree(temp);
+ }
+ }
+ PG_CATCH();
+ {
+ if (rscxt != NULL)
+ MemoryContextDelete(rscxt);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ systable_endscan(sscan);
+ heap_close(catalog, AccessShareLock);
+
+ relation->rsdesc = rsdesc;
+}
+
+/*
+ * Parse the supplied row-security policy, and insert/update a row
+ * of pg_rowsecurity catalog.
+ */
+static void
+InsertOrUpdatePolicyRow(Relation relation, char rseccmd, Node *clause)
+{
+ Oid relationId = RelationGetRelid(relation);
+ Oid rowsecId;
+ ParseState *pstate;
+ RangeTblEntry *rte;
+ Node *qual;
+ Relation catalog;
+ ScanKeyData skeys[2];
+ SysScanDesc sscan;
+ HeapTuple oldtup;
+ HeapTuple newtup;
+ Datum values[Natts_pg_rowsecurity];
+ bool isnull[Natts_pg_rowsecurity];
+ bool replaces[Natts_pg_rowsecurity];
+ ObjectAddress target;
+ ObjectAddress myself;
+
+ /* Parse the supplied clause */
+ pstate = make_parsestate(NULL);
+
+ rte = addRangeTableEntryForRelation(pstate, relation,
+ NULL, false, false);
+ addRTEtoQuery(pstate, rte, false, true, true);
+
+ qual = transformWhereClause(pstate, copyObject(clause),
+ EXPR_KIND_ROW_SECURITY,
+ "ROW SECURITY");
+ /* zero-clear */
+ memset(values, 0, sizeof(values));
+ memset(replaces, 0, sizeof(replaces));
+ memset(isnull, 0, sizeof(isnull));
+
+ /* Update or Insert an entry to pg_rowsecurity catalog */
+ catalog = heap_open(RowSecurityRelationId, RowExclusiveLock);
+
+ ScanKeyInit(&skeys[0],
+ Anum_pg_rowsecurity_rsecrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(RelationGetRelid(relation)));
+ ScanKeyInit(&skeys[1],
+ Anum_pg_rowsecurity_rseccmd,
+ BTEqualStrategyNumber, F_CHAREQ,
+ CharGetDatum(rseccmd));
+ sscan = systable_beginscan(catalog, RowSecurityRelidIndexId, true,
+ SnapshotNow, 2, skeys);
+ oldtup = systable_getnext(sscan);
+ if (HeapTupleIsValid(oldtup))
+ {
+ rowsecId = HeapTupleGetOid(oldtup);
+
+ replaces[Anum_pg_rowsecurity_rsecqual - 1] = true;
+ values[Anum_pg_rowsecurity_rsecqual - 1]
+ = CStringGetTextDatum(nodeToString(qual));
+
+ newtup = heap_modify_tuple(oldtup,
+ RelationGetDescr(catalog),
+ values, isnull, replaces);
+ simple_heap_update(catalog, &newtup->t_self, newtup);
+
+ deleteDependencyRecordsFor(RowSecurityRelationId, rowsecId, false);
+ }
+ else
+ {
+ values[Anum_pg_rowsecurity_rsecrelid - 1]
+ = ObjectIdGetDatum(relationId);
+ values[Anum_pg_rowsecurity_rseccmd - 1]
+ = CharGetDatum(rseccmd);
+ values[Anum_pg_rowsecurity_rsecqual - 1]
+ = CStringGetTextDatum(nodeToString(qual));
+ newtup = heap_form_tuple(RelationGetDescr(catalog),
+ values, isnull);
+ rowsecId = simple_heap_insert(catalog, newtup);
+ }
+ CatalogUpdateIndexes(catalog, newtup);
+
+ heap_freetuple(newtup);
+
+ /* records dependencies of row-security policy and relation/columns */
+ target.classId = RelationRelationId;
+ target.objectId = relationId;
+ target.objectSubId = 0;
+
+ myself.classId = RowSecurityRelationId;
+ myself.objectId = rowsecId;
+ myself.objectSubId = 0;
+
+ recordDependencyOn(&myself, &target, DEPENDENCY_AUTO);
+
+ recordDependencyOnExpr(&myself, qual, pstate->p_rtable,
+ DEPENDENCY_NORMAL);
+ free_parsestate(pstate);
+
+ systable_endscan(sscan);
+ heap_close(catalog, RowExclusiveLock);
+}
+
+/*
+ * Remove row-security policy row of pg_rowsecurity
+ */
+static void
+DeletePolicyRow(Relation relation, char rseccmd)
+{
+ Assert(rseccmd == ROWSECURITY_CMD_ALL);
+
+ if (relation->rsdesc)
+ {
+ ObjectAddress address;
+
+ address.classId = RowSecurityRelationId;
+ address.objectId = relation->rsdesc->rsall.rsecid;
+ address.objectSubId = 0;
+
+ performDeletion(&address, DROP_RESTRICT, 0);
+ }
+ else
+ {
+ /* Nothing to do here */
+ elog(INFO, "relation %s has no row-security policy, skipped",
+ RelationGetRelationName(relation));
+ }
+}
+
+/*
+ * Guts of row-security policy deletion.
+ */
+void
+RemoveRowSecurityById(Oid rowsecId)
+{
+ Relation catalog;
+ ScanKeyData skey;
+ SysScanDesc sscan;
+ HeapTuple tuple;
+ Relation rel;
+ Oid relid;
+
+ catalog = heap_open(RowSecurityRelationId, RowExclusiveLock);
+
+ ScanKeyInit(&skey,
+ ObjectIdAttributeNumber,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(rowsecId));
+ sscan = systable_beginscan(catalog, RowSecurityOidIndexId, true,
+ SnapshotNow, 1, &skey);
+ tuple = systable_getnext(sscan);
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "could not find tuple for row-security %u", rowsecId);
+
+ /*
+ * Open and exclusive-lock the relation the row-security belongs to.
+ */
+ relid = ((Form_pg_rowsecurity) GETSTRUCT(tuple))->rsecrelid;
+
+ rel = heap_open(relid, AccessExclusiveLock);
+
+ simple_heap_delete(catalog, &tuple->t_self);
+
+ /* Ensure relcache entries of other session being rebuilt */
+ CacheInvalidateRelcache(rel);
+
+ heap_close(rel, NoLock);
+
+ systable_endscan(sscan);
+ heap_close(catalog, RowExclusiveLock);
+}
+
+/*
+ * ALTER TABLE <name> SET ROW SECURITY (...) OR
+ * RESET ROW SECURITY
+ */
+void
+ATExecSetRowSecurity(Relation relation, const char *cmdname, Node *clause)
+{
+ Oid relid = RelationGetRelid(relation);
+ char rseccmd;
+
+ if (strcmp(cmdname, "all") == 0)
+ rseccmd = ROWSECURITY_CMD_ALL;
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("Row-security for \"%s\" is not implemented yet",
+ cmdname)));
+
+ if (clause != NULL)
+ {
+ InsertOrUpdatePolicyRow(relation, rseccmd, clause);
+
+ /*
+ * Also, turn on relhasrowsecurity, if not.
+ */
+ if (!RelationGetForm(relation)->relhasrowsecurity)
+ {
+ Relation class_rel = heap_open(RelationRelationId,
+ RowExclusiveLock);
+ HeapTuple tuple;
+
+ tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for relation %u", relid);
+
+ ((Form_pg_class) GETSTRUCT(tuple))->relhasrowsecurity = true;
+
+ simple_heap_update(class_rel, &tuple->t_self, tuple);
+ CatalogUpdateIndexes(class_rel, tuple);
+
+ heap_freetuple(tuple);
+ heap_close(class_rel, RowExclusiveLock);
+ }
+ }
+ else
+ DeletePolicyRow(relation, rseccmd);
+
+ CacheInvalidateRelcache(relation);
+}
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 31819cc..9cb8f1b 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -24,6 +24,7 @@
#include "access/htup_details.h"
#include "access/sysattr.h"
#include "access/xact.h"
+#include "catalog/heap.h"
#include "catalog/namespace.h"
#include "catalog/pg_type.h"
#include "commands/copy.h"
@@ -34,15 +35,19 @@
#include "libpq/pqformat.h"
#include "mb/pg_wchar.h"
#include "miscadmin.h"
+#include "nodes/makefuncs.h"
#include "optimizer/clauses.h"
#include "optimizer/planner.h"
+#include "optimizer/rowsecurity.h"
#include "parser/parse_relation.h"
+#include "parser/parsetree.h"
#include "rewrite/rewriteHandler.h"
#include "storage/fd.h"
#include "tcop/tcopprot.h"
#include "utils/acl.h"
#include "utils/builtins.h"
#include "utils/lsyscache.h"
+#include "utils/syscache.h"
#include "utils/memutils.h"
#include "utils/portal.h"
#include "utils/rel.h"
@@ -814,6 +819,21 @@ DoCopy(const CopyStmt *stmt, const char *queryString, uint64 *processed)
rel = heap_openrv(stmt->relation,
(is_from ? RowExclusiveLock : AccessShareLock));
+ tupDesc = RelationGetDescr(rel);
+ attnums = CopyGetAttnums(tupDesc, rel, stmt->attlist);
+
+ /*
+ * We have to run regular query, if the target relation has
+ * row-level security policy
+ */
+ if (copy_row_security_policy((CopyStmt *)stmt, rel, attnums))
+ {
+ heap_close(rel, NoLock); /* close with keeping lock */
+ relid = InvalidOid;
+ rel = NULL;
+ }
+ else
+ {
relid = RelationGetRelid(rel);
rte = makeNode(RangeTblEntry);
@@ -822,8 +842,6 @@ DoCopy(const CopyStmt *stmt, const char *queryString, uint64 *processed)
rte->relkind = rel->rd_rel->relkind;
rte->requiredPerms = required_access;
- tupDesc = RelationGetDescr(rel);
- attnums = CopyGetAttnums(tupDesc, rel, stmt->attlist);
foreach(cur, attnums)
{
int attno = lfirst_int(cur) -
@@ -835,6 +853,7 @@ DoCopy(const CopyStmt *stmt, const char *queryString, uint64 *processed)
rte->selectedCols = bms_add_member(rte->selectedCols, attno);
}
ExecCheckRTPerms(list_make1(rte), true);
+ }
}
else
{
@@ -1193,6 +1212,53 @@ ProcessCopyOptions(CopyState cstate,
}
/*
+ * Adjust Query tree constructed with row-level security feature.
+ * If WITH OIDS option was supplied, it adds Var node to reference
+ * object-id system column.
+ */
+static void
+fixup_oid_of_rls_query(Query *query)
+{
+ RangeTblEntry *subrte;
+ TargetEntry *subtle;
+ Var *subvar;
+ ListCell *cell;
+ Form_pg_attribute attform
+ = SystemAttributeDefinition(ObjectIdAttributeNumber, true);
+
+ subrte = rt_fetch((Index) 1, query->rtable);
+ Assert(subrte->rtekind == RTE_RELATION);
+
+ if (!SearchSysCacheExists2(ATTNUM,
+ ObjectIdGetDatum(subrte->relid),
+ Int16GetDatum(attform->attnum)))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_COLUMN),
+ errmsg("table \"%s\" does not have OIDs",
+ get_rel_name(subrte->relid))));
+
+ subvar = makeVar((Index) 1,
+ attform->attnum,
+ attform->atttypid,
+ attform->atttypmod,
+ attform->attcollation,
+ 0);
+ subtle = makeTargetEntry((Expr *) subvar,
+ 0,
+ pstrdup(NameStr(attform->attname)),
+ false);
+
+ query->targetList = list_concat(list_make1(subtle),
+ query->targetList);
+ /* adjust resno of TargetEntry */
+ foreach (cell, query->targetList)
+ {
+ subtle = lfirst(cell);
+ subtle->resno++;
+ }
+}
+
+/*
* Common setup routines used by BeginCopyFrom and BeginCopyTo.
*
* Iff <binary>, unload or reload in the binary format, as opposed to the
@@ -1264,6 +1330,25 @@ BeginCopy(bool is_from,
Assert(!is_from);
cstate->rel = NULL;
+ /*
+ * In case when regular COPY TO was replaced because of row-level
+ * security, "raw_query" node have already analyzed / rewritten
+ * query tree.
+ */
+ if (IsA(raw_query, Query))
+ {
+ query = (Query *) raw_query;
+
+ Assert(query->querySource == QSRC_ROW_SECURITY);
+ if (cstate->oids)
+ {
+ fixup_oid_of_rls_query(query);
+ cstate->oids = false;
+ }
+ attnamelist = NIL;
+ }
+ else
+ {
/* Don't allow COPY w/ OIDs from a select */
if (cstate->oids)
ereport(ERROR,
@@ -1288,6 +1373,7 @@ BeginCopy(bool is_from,
elog(ERROR, "unexpected rewrite result");
query = (Query *) linitial(rewritten);
+ }
/* The grammar allows SELECT INTO, but we don't support that */
if (query->utilityStmt != NULL &&
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 328e2a8..0374629 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -992,6 +992,7 @@ EventTriggerSupportsObjectClass(ObjectClass objclass)
case OCLASS_USER_MAPPING:
case OCLASS_DEFACL:
case OCLASS_EXTENSION:
+ case OCLASS_ROWSECURITY:
return true;
case MAX_OCLASS:
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index f56ef28..770bcc2 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -37,6 +37,7 @@
#include "catalog/pg_inherits_fn.h"
#include "catalog/pg_namespace.h"
#include "catalog/pg_opclass.h"
+#include "catalog/pg_rowsecurity.h"
#include "catalog/pg_tablespace.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
@@ -2787,6 +2788,8 @@ AlterTableGetLockLevel(List *cmds)
case AT_SetTableSpace: /* must rewrite heap */
case AT_DropNotNull: /* may change some SQL plans */
case AT_SetNotNull:
+ case AT_SetRowSecurity:
+ case AT_ResetRowSecurity:
case AT_GenericOptions:
case AT_AlterColumnGenericOptions:
cmd_lockmode = AccessExclusiveLock;
@@ -3155,6 +3158,8 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
case AT_DropInherit: /* NO INHERIT */
case AT_AddOf: /* OF */
case AT_DropOf: /* NOT OF */
+ case AT_SetRowSecurity:
+ case AT_ResetRowSecurity:
ATSimplePermissions(rel, ATT_TABLE);
/* These commands never recurse */
/* No command-specific prep needed */
@@ -3440,6 +3445,12 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab, Relation rel,
case AT_DropOf:
ATExecDropOf(rel, lockmode);
break;
+ case AT_SetRowSecurity:
+ ATExecSetRowSecurity(rel, cmd->name, (Node *) cmd->def);
+ break;
+ case AT_ResetRowSecurity:
+ ATExecSetRowSecurity(rel, cmd->name, NULL);
+ break;
case AT_GenericOptions:
ATExecGenericOptions(rel, (List *) cmd->def);
break;
@@ -7788,6 +7799,22 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
Assert(defaultexpr);
break;
+ case OCLASS_ROWSECURITY:
+ /*
+ * A row-level security policy can depend on a column in case
+ * when the policy clause references a particular column.
+ * Due to same reason why TRIGGER ... WHEN does not support
+ * to change column's type being referenced in clause, row-
+ * level security policy also does not support it.
+ */
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot alter type of a column used in a row-level security policy"),
+ errdetail("%s depends on column \"%s\"",
+ getObjectDescription(&foundObject),
+ colName)));
+ break;
+
case OCLASS_PROC:
case OCLASS_TYPE:
case OCLASS_CAST:
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index b5b8d63..0835bda 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -1933,6 +1933,7 @@ _copyAppendRelInfo(const AppendRelInfo *from)
COPY_SCALAR_FIELD(parent_relid);
COPY_SCALAR_FIELD(child_relid);
+ COPY_SCALAR_FIELD(child_result);
COPY_SCALAR_FIELD(parent_reltype);
COPY_SCALAR_FIELD(child_reltype);
COPY_NODE_FIELD(translated_vars);
@@ -1974,6 +1975,7 @@ _copyRangeTblEntry(const RangeTblEntry *from)
COPY_SCALAR_FIELD(relkind);
COPY_NODE_FIELD(subquery);
COPY_SCALAR_FIELD(security_barrier);
+ COPY_SCALAR_FIELD(rowsec_relid);
COPY_SCALAR_FIELD(jointype);
COPY_NODE_FIELD(joinaliasvars);
COPY_NODE_FIELD(funcexpr);
@@ -2432,6 +2434,7 @@ _copyQuery(const Query *from)
COPY_SCALAR_FIELD(canSetTag);
COPY_NODE_FIELD(utilityStmt);
COPY_SCALAR_FIELD(resultRelation);
+ COPY_SCALAR_FIELD(sourceRelation);
COPY_SCALAR_FIELD(hasAggs);
COPY_SCALAR_FIELD(hasWindowFuncs);
COPY_SCALAR_FIELD(hasSubLinks);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 3f96595..f688275 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -805,6 +805,7 @@ _equalAppendRelInfo(const AppendRelInfo *a, const AppendRelInfo *b)
{
COMPARE_SCALAR_FIELD(parent_relid);
COMPARE_SCALAR_FIELD(child_relid);
+ COMPARE_SCALAR_FIELD(child_result);
COMPARE_SCALAR_FIELD(parent_reltype);
COMPARE_SCALAR_FIELD(child_reltype);
COMPARE_NODE_FIELD(translated_vars);
@@ -840,6 +841,7 @@ _equalQuery(const Query *a, const Query *b)
COMPARE_SCALAR_FIELD(canSetTag);
COMPARE_NODE_FIELD(utilityStmt);
COMPARE_SCALAR_FIELD(resultRelation);
+ COMPARE_SCALAR_FIELD(sourceRelation);
COMPARE_SCALAR_FIELD(hasAggs);
COMPARE_SCALAR_FIELD(hasWindowFuncs);
COMPARE_SCALAR_FIELD(hasSubLinks);
@@ -2222,6 +2224,7 @@ _equalRangeTblEntry(const RangeTblEntry *a, const RangeTblEntry *b)
COMPARE_SCALAR_FIELD(relkind);
COMPARE_NODE_FIELD(subquery);
COMPARE_SCALAR_FIELD(security_barrier);
+ COMPARE_SCALAR_FIELD(rowsec_relid);
COMPARE_SCALAR_FIELD(jointype);
COMPARE_NODE_FIELD(joinaliasvars);
COMPARE_NODE_FIELD(funcexpr);
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 42d6621..2b265dd 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -1869,8 +1869,11 @@ query_tree_walker(Query *query,
if (walker((Node *) query->targetList, context))
return true;
- if (walker((Node *) query->returningList, context))
- return true;
+ if (!(flags & QTW_IGNORE_RETURNING))
+ {
+ if (walker((Node *) query->returningList, context))
+ return true;
+ }
if (walker((Node *) query->jointree, context))
return true;
if (walker(query->setOperations, context))
@@ -2583,7 +2586,10 @@ query_tree_mutator(Query *query,
}
MUTATE(query->targetList, query->targetList, List *);
- MUTATE(query->returningList, query->returningList, List *);
+ if (!(flags & QTW_IGNORE_RETURNING))
+ MUTATE(query->returningList, query->returningList, List *);
+ else
+ query->returningList = copyObject(query->returningList);
MUTATE(query->jointree, query->jointree, FromExpr *);
MUTATE(query->setOperations, query->setOperations, Node *);
MUTATE(query->havingQual, query->havingQual, Node *);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index b2183f4..81d7774 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -1920,6 +1920,7 @@ _outAppendRelInfo(StringInfo str, const AppendRelInfo *node)
WRITE_UINT_FIELD(parent_relid);
WRITE_UINT_FIELD(child_relid);
+ WRITE_UINT_FIELD(child_result);
WRITE_OID_FIELD(parent_reltype);
WRITE_OID_FIELD(child_reltype);
WRITE_NODE_FIELD(translated_vars);
@@ -2233,6 +2234,7 @@ _outQuery(StringInfo str, const Query *node)
appendStringInfo(str, " :utilityStmt <>");
WRITE_INT_FIELD(resultRelation);
+ WRITE_INT_FIELD(sourceRelation);
WRITE_BOOL_FIELD(hasAggs);
WRITE_BOOL_FIELD(hasWindowFuncs);
WRITE_BOOL_FIELD(hasSubLinks);
@@ -2357,6 +2359,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node)
case RTE_SUBQUERY:
WRITE_NODE_FIELD(subquery);
WRITE_BOOL_FIELD(security_barrier);
+ WRITE_OID_FIELD(rowsec_relid);
break;
case RTE_JOIN:
WRITE_ENUM_FIELD(jointype, JoinType);
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 3a16e9d..1a3da23 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -199,6 +199,7 @@ _readQuery(void)
READ_BOOL_FIELD(canSetTag);
READ_NODE_FIELD(utilityStmt);
READ_INT_FIELD(resultRelation);
+ READ_INT_FIELD(sourceRelation);
READ_BOOL_FIELD(hasAggs);
READ_BOOL_FIELD(hasWindowFuncs);
READ_BOOL_FIELD(hasSubLinks);
@@ -1195,6 +1196,7 @@ _readRangeTblEntry(void)
case RTE_SUBQUERY:
READ_NODE_FIELD(subquery);
READ_BOOL_FIELD(security_barrier);
+ READ_OID_FIELD(rowsec_relid);
break;
case RTE_JOIN:
READ_ENUM_FIELD(jointype, JoinType);
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index d80c264..976b9c0 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -33,6 +33,7 @@
#include "optimizer/planmain.h"
#include "optimizer/planner.h"
#include "optimizer/prep.h"
+#include "optimizer/rowsecurity.h"
#include "optimizer/subselect.h"
#include "optimizer/tlist.h"
#include "parser/analyze.h"
@@ -176,6 +177,7 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams)
glob->lastPHId = 0;
glob->lastRowMarkId = 0;
glob->transientPlan = false;
+ glob->planUserId = InvalidOid;
/* Determine what fraction of the plan is likely to be scanned */
if (cursorOptions & CURSOR_OPT_FAST_PLAN)
@@ -253,6 +255,7 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams)
result->relationOids = glob->relationOids;
result->invalItems = glob->invalItems;
result->nParamExec = glob->nParamExec;
+ result->planUserId = glob->planUserId;
return result;
}
@@ -402,6 +405,19 @@ subquery_planner(PlannerGlobal *glob, Query *parse,
expand_inherited_tables(root);
/*
+ * Apply row-security policy of the relation being referenced,
+ * if configured with either of built-in or extension's features.
+ * RangeTblEntry of the relation with row-security policy shall
+ * be replaced with a row-security subquery that has simple scan
+ * on the target relation with row-security policy qualifiers.
+ *
+ * This routine assumes PlannerInfo is already handled with
+ * expand_inherited_tables, thus, AppendRelInfo or PlanRowMark
+ * have valid information.
+ */
+ apply_row_security_policy(root);
+
+ /*
* Set hasHavingQual to remember if HAVING clause is present. Needed
* because preprocess_expression will reduce a constant-true condition to
* an empty qual list ... but "HAVING TRUE" is not a semantic no-op.
@@ -865,6 +881,8 @@ inheritance_planner(PlannerInfo *root)
newrti = list_length(subroot.parse->rtable) + 1;
ChangeVarNodes((Node *) subroot.parse, rti, newrti, 0);
ChangeVarNodes((Node *) subroot.rowMarks, rti, newrti, 0);
+ if (subroot.parse->sourceRelation == rti)
+ subroot.parse->sourceRelation = newrti;
rte = copyObject(rte);
subroot.parse->rtable = lappend(subroot.parse->rtable,
rte);
@@ -928,7 +946,10 @@ inheritance_planner(PlannerInfo *root)
root->init_plans = subroot.init_plans;
/* Build list of target-relation RT indexes */
- resultRelations = lappend_int(resultRelations, appinfo->child_relid);
+ resultRelations = lappend_int(resultRelations,
+ (appinfo->child_result > 0 ?
+ appinfo->child_result :
+ appinfo->child_relid));
/* Build list of per-relation RETURNING targetlists */
if (parse->returningList)
diff --git a/src/backend/optimizer/prep/preptlist.c b/src/backend/optimizer/prep/preptlist.c
index fb67f9e..4abf434 100644
--- a/src/backend/optimizer/prep/preptlist.c
+++ b/src/backend/optimizer/prep/preptlist.c
@@ -37,8 +37,48 @@
static List *expand_targetlist(List *tlist, int command_type,
- Index result_relation, List *range_table);
+ 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 Var that references sub-queries being originated from regular
+ * relations with row-level security policy due to nature of sub-query
+ * that has no system-column.
+ */
+static AttrNumber
+lookup_varattno(AttrNumber attno, Index rt_index, List *rtables)
+{
+ RangeTblEntry *rte = rt_fetch(rt_index, rtables);
+
+ if (rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_SECURITY)
+ {
+ ListCell *cell;
+
+ foreach (cell, rte->subquery->targetList)
+ {
+ TargetEntry *tle = lfirst(cell);
+ Var *var;
+
+ if (IsA(tle->expr, Const))
+ continue;
+
+ var = (Var *) tle->expr;
+ Assert(IsA(var, Var));
+
+ if (var->varattno == attno)
+ return tle->resno;
+ }
+ elog(ERROR, "invalid attno %d on row-security subquery target-list",
+ attno);
+ }
+ return attno;
+}
/*
* preprocess_targetlist
@@ -73,8 +113,13 @@ preprocess_targetlist(PlannerInfo *root, List *tlist)
* 10/94
*/
if (command_type == CMD_INSERT || command_type == CMD_UPDATE)
+ {
+ Index source_relation = (parse->sourceRelation > 0 ?
+ parse->sourceRelation :
+ result_relation);
tlist = expand_targetlist(tlist, command_type,
- result_relation, range_table);
+ source_relation, range_table);
+ }
/*
* Add necessary junk columns for rowmarked rels. These values are needed
@@ -96,7 +141,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,
@@ -112,7 +158,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,
@@ -130,7 +177,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,
@@ -299,7 +346,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 e249628..0cbb035 100644
--- a/src/backend/optimizer/prep/prepunion.c
+++ b/src/backend/optimizer/prep/prepunion.c
@@ -55,6 +55,7 @@ typedef struct
{
PlannerInfo *root;
AppendRelInfo *appinfo;
+ bool in_returning;
} adjust_appendrel_attrs_context;
static Plan *recurse_set_operations(Node *setOp, PlannerInfo *root,
@@ -1594,6 +1595,7 @@ adjust_appendrel_attrs(PlannerInfo *root, Node *node, AppendRelInfo *appinfo)
context.root = root;
context.appinfo = appinfo;
+ context.in_returning = false;
/*
* Must be prepared to start with a Query or a bare expression tree.
@@ -1605,10 +1607,29 @@ adjust_appendrel_attrs(PlannerInfo *root, Node *node, AppendRelInfo *appinfo)
newnode = query_tree_mutator((Query *) node,
adjust_appendrel_attrs_mutator,
(void *) &context,
- QTW_IGNORE_RC_SUBQUERIES);
+ QTW_IGNORE_RC_SUBQUERIES |
+ QTW_IGNORE_RETURNING);
+ /*
+ * Returning clause on the relation being replaced with row-
+ * security subquery shall be handled in a special way, because
+ * of no system columns on subquery.
+ * Var references to system column or whole-row reference need
+ * to be adjusted to reference artificial columns on behalf of
+ * the underlying these columns, however, RETURNGIN clause is
+ * an exception because its Var nodes are evaluated towards
+ * the "raw" target relation, not a fetched tuple.
+ */
+ context.in_returning = true;
+ newnode->returningList = (List *)
+ expression_tree_mutator((Node *) newnode->returningList,
+ adjust_appendrel_attrs_mutator,
+ (void *) &context);
if (newnode->resultRelation == appinfo->parent_relid)
{
- newnode->resultRelation = appinfo->child_relid;
+ newnode->resultRelation = (appinfo->child_result > 0 ?
+ appinfo->child_result :
+ appinfo->child_relid);
+ newnode->sourceRelation = appinfo->child_relid;
/* Fix tlist resnos too, if it's inherited UPDATE */
if (newnode->commandType == CMD_UPDATE)
newnode->targetList =
@@ -1624,6 +1645,49 @@ adjust_appendrel_attrs(PlannerInfo *root, Node *node, AppendRelInfo *appinfo)
}
static Node *
+fixup_var_on_rowsec_subquery(RangeTblEntry *rte, Var *var)
+{
+ ListCell *cell;
+
+ Assert(rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_SECURITY);
+ /*
+ * In case when row-level security policy is applied on the referenced
+ * table, its RangeTblEntry (RTE_RELATION) is replaced with sub-query
+ * to filter out unprivileged rows of underlying relation.
+ * Even though reference to this sub-query should perform as if ones
+ * to real relations, system column has to be cared in special way
+ * due to the nature of sub-query.
+ * Target-entries that reference system columns should be added on
+ * rowlevelsec.c, so all we need to do here is looking up underlying
+ * target-list that can reference underlying system column, and fix-
+ * up varattno of the referencing Var node with resno of TargetEntry.
+ */
+ foreach (cell, rte->subquery->targetList)
+ {
+ TargetEntry *subtle = lfirst(cell);
+
+ if (IsA(subtle->expr, Var))
+ {
+ Var *subvar = (Var *) subtle->expr;
+ Var *newnode;
+
+ if (subvar->varattno == var->varattno)
+ {
+ newnode = copyObject(var);
+ newnode->varattno = subtle->resno;
+ return (Node *)newnode;
+ }
+ }
+ else
+ Assert(IsA(subtle->expr, Const));
+ }
+ elog(ERROR, "could not find artificial column of %d, relid %u",
+ var->varattno, var->varno);
+ return NULL;
+}
+
+static Node *
adjust_appendrel_attrs_mutator(Node *node,
adjust_appendrel_attrs_context *context)
{
@@ -1638,8 +1702,12 @@ adjust_appendrel_attrs_mutator(Node *node,
if (var->varlevelsup == 0 &&
var->varno == appinfo->parent_relid)
{
- var->varno = appinfo->child_relid;
+ var->varno = (context->in_returning &&
+ appinfo->child_result > 0 ?
+ appinfo->child_result :
+ appinfo->child_relid);
var->varnoold = appinfo->child_relid;
+
if (var->varattno > 0)
{
Node *newnode;
@@ -1664,6 +1732,14 @@ adjust_appendrel_attrs_mutator(Node *node,
*/
if (OidIsValid(appinfo->child_reltype))
{
+ Query *parse = context->root->parse;
+ RangeTblEntry *rte = rt_fetch(appinfo->child_relid,
+ parse->rtable);
+ if (!context->in_returning &&
+ rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_SECURITY)
+ var = (Var *)fixup_var_on_rowsec_subquery(rte, var);
+
Assert(var->vartype == appinfo->parent_reltype);
if (appinfo->parent_reltype != appinfo->child_reltype)
{
@@ -1708,7 +1784,18 @@ adjust_appendrel_attrs_mutator(Node *node,
return (Node *) rowexpr;
}
}
- /* system attributes don't need any other translation */
+ else
+ {
+ Query *parse = context->root->parse;
+ RangeTblEntry *rte;
+
+ rte = rt_fetch(appinfo->child_relid, parse->rtable);
+
+ if (!context->in_returning &&
+ rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_SECURITY)
+ return fixup_var_on_rowsec_subquery(rte, var);
+ }
}
return (Node *) var;
}
diff --git a/src/backend/optimizer/util/Makefile b/src/backend/optimizer/util/Makefile
index 3b2d16b..3f5cb19 100644
--- a/src/backend/optimizer/util/Makefile
+++ b/src/backend/optimizer/util/Makefile
@@ -13,6 +13,6 @@ top_builddir = ../../../..
include $(top_builddir)/src/Makefile.global
OBJS = clauses.o joininfo.o pathnode.o placeholder.o plancat.o predtest.o \
- relnode.o restrictinfo.o tlist.o var.o
+ relnode.o restrictinfo.o tlist.o var.o rowsecurity.o
include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/optimizer/util/rowsecurity.c b/src/backend/optimizer/util/rowsecurity.c
new file mode 100644
index 0000000..d686dfa
--- /dev/null
+++ b/src/backend/optimizer/util/rowsecurity.c
@@ -0,0 +1,733 @@
+/*
+ * optimizer/util/rowsecurity.c
+ * Routines to support row-security feature
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ */
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "access/htup_details.h"
+#include "access/sysattr.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_inherits_fn.h"
+#include "catalog/pg_rowsecurity.h"
+#include "catalog/pg_type.h"
+#include "miscadmin.h"
+#include "nodes/makefuncs.h"
+#include "nodes/nodeFuncs.h"
+#include "nodes/plannodes.h"
+#include "optimizer/clauses.h"
+#include "optimizer/prep.h"
+#include "optimizer/rowsecurity.h"
+#include "parser/parsetree.h"
+#include "rewrite/rewriteHandler.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "tcop/utility.h"
+
+/* flags to pull row-security policy */
+#define RSEC_FLAG_HAS_SUBLINKS 0x0001
+
+/* hook to allow extensions to apply their own security policy */
+row_security_policy_hook_type row_security_policy_hook = NULL;
+
+/*
+ * make_artificial_column
+ *
+ * It makes a target-entry node that references underlying column.
+ * Its tle->expr is usualy Var node, but may be Const for dummy NULL
+ * if the supplied attribute was already dropped.
+ */
+static TargetEntry *
+make_artificial_column(RangeTblEntry *subrte, AttrNumber attnum)
+{
+ Expr *expr;
+ char *resname;
+
+ Assert(subrte->rtekind == RTE_RELATION && OidIsValid(subrte->relid));
+ if (attnum == InvalidAttrNumber)
+ {
+ expr = (Expr *) makeWholeRowVar(subrte, (Index) 1, 0, false);
+ resname = get_rel_name(subrte->relid);
+ }
+ else
+ {
+ HeapTuple tuple;
+ Form_pg_attribute attform;
+
+ tuple = SearchSysCache2(ATTNUM,
+ ObjectIdGetDatum(subrte->relid),
+ Int16GetDatum(attnum));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for attribute %d of relation %u",
+ attnum, subrte->relid);
+ attform = (Form_pg_attribute) GETSTRUCT(tuple);
+
+ if (attform->attisdropped)
+ {
+ char namebuf[NAMEDATALEN];
+
+ /* Insert NULL just for a placeholder of dropped column */
+ expr = (Expr *) makeConst(INT4OID,
+ -1,
+ InvalidOid,
+ sizeof(int32),
+ (Datum) 0,
+ true, /* isnull */
+ true); /* byval */
+ sprintf(namebuf, "dummy-%d", (int)attform->attnum);
+ resname = pstrdup(namebuf);
+ }
+ else
+ {
+ expr = (Expr *) makeVar((Index) 1,
+ attform->attnum,
+ attform->atttypid,
+ attform->atttypmod,
+ attform->attcollation,
+ 0);
+ resname = pstrdup(NameStr(attform->attname));
+ }
+ ReleaseSysCache(tuple);
+ }
+ return makeTargetEntry(expr, -1, resname, false);
+}
+
+/*
+ * lookup_artificial_column
+ *
+ * It looks-up resource number of the target-entry relevant to the given
+ * Var-node that references the row-security subquery. If required column
+ * is not in the subquery's target-list, this function also adds new one
+ * and returns its resource number.
+ */
+static AttrNumber
+lookup_artificial_column(PlannerInfo *root,
+ RangeTblEntry *rte, AttrNumber varattno)
+{
+ Query *subqry;
+ RangeTblEntry *subrte;
+ TargetEntry *subtle;
+ ListCell *cell;
+
+ Assert(rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_SECURITY);
+
+ subqry = rte->subquery;
+ foreach (cell, subqry->targetList)
+ {
+ subtle = lfirst(cell);
+
+ /*
+ * If referenced artifical column is already constructed on the
+ * target-list of row-security subquery, nothing to do any more.
+ */
+ if (IsA(subtle->expr, Var))
+ {
+ Var *subvar = (Var *)subtle->expr;
+
+ Assert(subvar->varno == 1);
+ if (subvar->varattno == varattno)
+ return subtle->resno;
+ }
+ }
+
+ /*
+ * OK, we don't have an artifical column relevant to the required ones,
+ * so let's create a new artifical column on demand.
+ */
+ subrte = rt_fetch((Index) 1, subqry->rtable);
+ subtle = make_artificial_column(subrte, varattno);
+ subtle->resno = list_length(subqry->targetList) + 1;
+
+ subqry->targetList = lappend(subqry->targetList, subtle);
+ rte->eref->colnames = lappend(rte->eref->colnames,
+ makeString(pstrdup(subtle->resname)));
+ return subtle->resno;
+}
+
+/*
+ * fixup_varnode_walker
+ *
+ * It recursively fixes up references to the relation to be replaced by
+ * row-security sub-query, and adds artificial columns relevant to the
+ * underlying system columns or whole row-reference on demand.
+ */
+typedef struct {
+ PlannerInfo *root;
+ int varlevelsup;
+ Index *vartrans;
+} fixup_varnode_context;
+
+static bool
+fixup_varnode_walker(Node *node, fixup_varnode_context *context)
+{
+ if (node == NULL)
+ return false;
+
+ if (IsA(node, Var))
+ {
+ Var *var = (Var *) node;
+ List *rtable = context->root->parse->rtable;
+ RangeTblEntry *rte;
+ ListCell *cell;
+
+ /*
+ * Ignore it, if Var node does not reference the Query currently
+ * we focues on.
+ */
+ if (var->varlevelsup != context->varlevelsup)
+ return false;
+
+ /*
+ * Var nodes that reference the relation being replaced by row-
+ * security sub-query has to be adjusted; to reference the sub-
+ * query, instead of the original relation.
+ */
+ if (context->vartrans[var->varno] != 0)
+ {
+ rte = rt_fetch(context->vartrans[var->varno], rtable);
+ if (rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_SECURITY)
+ {
+ var->varno = var->varnoold = context->vartrans[var->varno];
+ var->varattno = lookup_artificial_column(context->root,
+ rte, var->varattno);
+ }
+ }
+ else
+ {
+ rte = rt_fetch(var->varno, rtable);
+ if (!rte->inh)
+ return false;
+
+ foreach (cell, context->root->append_rel_list)
+ {
+ AppendRelInfo *appinfo = lfirst(cell);
+ RangeTblEntry *child_rte;
+
+ if (appinfo->parent_relid != var->varno)
+ continue;
+
+ if (var->varattno > InvalidAttrNumber)
+ continue;
+
+ child_rte = rt_fetch(appinfo->child_relid, rtable);
+ if (child_rte->rtekind == RTE_SUBQUERY &&
+ child_rte->subquery->querySource == QSRC_ROW_SECURITY)
+ (void) lookup_artificial_column(context->root,
+ child_rte,
+ var->varattno);
+ }
+ }
+ }
+ else if (IsA(node, RangeTblRef))
+ {
+ RangeTblRef *rtr = (RangeTblRef *) node;
+
+ if (context->varlevelsup == 0 &&
+ context->vartrans[rtr->rtindex] != 0)
+ rtr->rtindex = context->vartrans[rtr->rtindex];
+ }
+ else if (IsA(node, Query))
+ {
+ bool result;
+
+ context->varlevelsup++;
+ result = query_tree_walker((Query *) node,
+ fixup_varnode_walker,
+ (void *) context, 0);
+ context->varlevelsup--;
+
+ return result;
+ }
+ return expression_tree_walker(node,
+ fixup_varnode_walker,
+ (void *) context);
+}
+
+/*
+ * check_infinite_recursion
+ *
+ * It is a wrong row-security configuration, if we try to expand
+ * the relation inside of row-security subquery originated from
+ * same relation!
+ */
+static void
+check_infinite_recursion(PlannerInfo *root, Oid relid)
+{
+ PlannerInfo *parent = root->parent_root;
+
+ if (parent && parent->parse->querySource == QSRC_ROW_SECURITY)
+ {
+ RangeTblEntry *rte = rt_fetch(1, parent->parse->rtable);
+
+ Assert(rte->rtekind == RTE_RELATION && OidIsValid(rte->relid));
+
+ if (relid == rte->relid)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("infinite recursion detected for relation \"%s\"",
+ get_rel_name(relid))));
+ check_infinite_recursion(parent, relid);
+ }
+}
+
+/*
+ * expand_rtentry_with_policy
+ *
+ * It extends a range-table entry of row-security sub-query with supplied
+ * security policy, and append it on the parse->rtable.
+ * This sub-query contains artificial columns that reference underlying
+ * regular columns (at least, references to system column or whole of
+ * table reference shall be added on demand), and simple scan on the
+ * target relation.
+ * Any Var nodes that referenced the relation pointed by rtindex shall
+ * be adjusted to reference this sub-query instead. walker
+ *
+ */
+static Index
+expand_rtentry_with_policy(PlannerInfo *root, Index rtindex,
+ Expr *qual, int flags)
+{
+ Query *parse = root->parse;
+ RangeTblEntry *rte = rt_fetch(rtindex, parse->rtable);
+ Query *subqry;
+ RangeTblEntry *subrte;
+ RangeTblRef *subrtr;
+ TargetEntry *subtle;
+ RangeTblEntry *newrte;
+ HeapTuple tuple;
+ AttrNumber nattrs;
+ AttrNumber attnum;
+ List *targetList = NIL;
+ List *colNameList = NIL;
+ PlanRowMark *rowmark;
+
+ Assert(rte->rtekind == RTE_RELATION && !rte->inh);
+
+ /* check recursion to prevent infinite loop */
+ check_infinite_recursion(root, rte->relid);
+
+ /* Expand views inside SubLink node */
+ if (flags & RSEC_FLAG_HAS_SUBLINKS)
+ QueryRewriteExpr((Node *)qual, list_make1_oid(rte->relid));
+
+ /*
+ * Construction of sub-query
+ */
+ subqry = (Query *) makeNode(Query);
+ subqry->commandType = CMD_SELECT;
+ subqry->querySource = QSRC_ROW_SECURITY;
+
+ subrte = copyObject(rte);
+ subqry->rtable = list_make1(subrte);
+
+ subrtr = makeNode(RangeTblRef);
+ subrtr->rtindex = 1;
+ subqry->jointree = makeFromExpr(list_make1(subrtr), (Node *) qual);
+ if (flags & RSEC_FLAG_HAS_SUBLINKS)
+ subqry->hasSubLinks = true;
+
+ /*
+ * Construction of TargetEntries that reference underlying columns.
+ */
+ tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(rte->relid));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for relation %u", rte->relid);
+ nattrs = ((Form_pg_class) GETSTRUCT(tuple))->relnatts;
+ ReleaseSysCache(tuple);
+
+ for (attnum = 1; attnum <= nattrs; attnum++)
+ {
+ subtle = make_artificial_column(subrte, attnum);
+ subtle->resno = list_length(targetList) + 1;
+ Assert(subtle->resno == attnum);
+
+ targetList = lappend(targetList, subtle);
+ colNameList = lappend(colNameList,
+ makeString(pstrdup(subtle->resname)));
+ }
+ subqry->targetList = targetList;
+
+ /* Expand RengeTblEntry with this sub-query */
+ newrte = makeNode(RangeTblEntry);
+ newrte->rtekind = RTE_SUBQUERY;
+ newrte->subquery = subqry;
+ newrte->security_barrier = true;
+ newrte->rowsec_relid = rte->relid;
+ newrte->eref = makeAlias(get_rel_name(rte->relid), colNameList);
+
+ parse->rtable = lappend(parse->rtable, newrte);
+
+ /* Push-down rowmark, if needed */
+ rowmark = get_plan_rowmark(root->rowMarks, rtindex);
+ if (rowmark)
+ {
+ /*
+ * In case of inherited children, rti/prti of rowmark shall be
+ * fixed up later, on inheritance_planner().
+ */
+ if (rowmark->rti == rowmark->prti)
+ rowmark->rti = rowmark->prti = list_length(parse->rtable);
+ else
+ rowmark->rti = list_length(parse->rtable);
+
+ lookup_artificial_column(root, newrte, SelfItemPointerAttributeNumber);
+ lookup_artificial_column(root, newrte, TableOidAttributeNumber);
+ }
+
+ return list_length(parse->rtable);
+}
+
+/*
+ * pull_row_security_policy
+ *
+ * It pulls the configured row-security policy of both built-in and
+ * extensions. If any, it returns expression tree.
+ */
+static Expr *
+pull_row_security_policy(CmdType cmd, Relation relation, int *p_flags)
+{
+ Expr *quals = NULL;
+ int flags = 0;
+
+ /*
+ * Pull the row-security policy configured with built-in features,
+ * if unprivileged users. Please note that superuser can bypass it.
+ */
+ if (relation->rsdesc && !superuser())
+ {
+ RowSecurityDesc *rsdesc = relation->rsdesc;
+
+ quals = copyObject(rsdesc->rsall.qual);
+ if (rsdesc->rsall.hassublinks)
+ flags |= RSEC_FLAG_HAS_SUBLINKS;
+ }
+
+ /*
+ * Also, ask extensions whether they want to apply their own
+ * row-security policy. If both built-in and extension has
+ * their own policy, it shall be merged.
+ */
+ if (row_security_policy_hook)
+ {
+ List *temp;
+
+ temp = (*row_security_policy_hook)(cmd, relation);
+ if (temp != NIL)
+ {
+ if ((flags & RSEC_FLAG_HAS_SUBLINKS) == 0 &&
+ contain_subplans((Node *) temp))
+ flags |= RSEC_FLAG_HAS_SUBLINKS;
+
+ if (quals != NULL)
+ temp = lappend(temp, quals);
+
+ if (list_length(temp) == 1)
+ quals = (Expr *)list_head(temp);
+ else if (list_length(temp) > 1)
+ quals = makeBoolExpr(AND_EXPR, temp, -1);
+ }
+ }
+ *p_flags = flags;
+ return quals;
+}
+
+/*
+ * copy_row_security_policy
+ *
+ * It construct a row-security subquery instead of raw COPY TO statement,
+ * if target relation has a row-level security policy
+ */
+bool
+copy_row_security_policy(CopyStmt *stmt, Relation rel, List *attnums)
+{
+ Expr *quals;
+ int flags;
+ Query *parse;
+ RangeTblEntry *rte;
+ RangeTblRef *rtr;
+ TargetEntry *tle;
+ Var *var;
+ ListCell *cell;
+
+ if (stmt->is_from)
+ return false;
+
+ quals = pull_row_security_policy(CMD_SELECT, rel, &flags);
+ if (!quals)
+ return false;
+
+ parse = (Query *) makeNode(Query);
+ parse->commandType = CMD_SELECT;
+ parse->querySource = QSRC_ROW_SECURITY;
+
+ rte = makeNode(RangeTblEntry);
+ rte->rtekind = RTE_RELATION;
+ rte->relid = RelationGetRelid(rel);
+ rte->relkind = RelationGetForm(rel)->relkind;
+
+ foreach (cell, attnums)
+ {
+ HeapTuple tuple;
+ Form_pg_attribute attform;
+ AttrNumber attno = lfirst_int(cell);
+
+ tuple = SearchSysCache2(ATTNUM,
+ ObjectIdGetDatum(RelationGetRelid(rel)),
+ Int16GetDatum(attno));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for attribute %d of relation %s",
+ attno, RelationGetRelationName(rel));
+ attform = (Form_pg_attribute) GETSTRUCT(tuple);
+
+ var = makeVar((Index) 1,
+ attform->attnum,
+ attform->atttypid,
+ attform->atttypmod,
+ attform->attcollation,
+ 0);
+ tle = makeTargetEntry((Expr *) var,
+ list_length(parse->targetList) + 1,
+ pstrdup(NameStr(attform->attname)),
+ false);
+ parse->targetList = lappend(parse->targetList, tle);
+
+ ReleaseSysCache(tuple);
+
+ rte->selectedCols = bms_add_member(rte->selectedCols,
+ attno - FirstLowInvalidHeapAttributeNumber);
+ }
+ rte->inFromCl = true;
+ rte->requiredPerms = ACL_SELECT;
+
+ rtr = makeNode(RangeTblRef);
+ rtr->rtindex = 1;
+
+ parse->jointree = makeFromExpr(list_make1(rtr), (Node *) quals);
+ parse->rtable = list_make1(rte);
+ if (flags & RSEC_FLAG_HAS_SUBLINKS)
+ parse->hasSubLinks = true;
+
+ stmt->query = (Node *) parse;
+
+ return true;
+}
+
+/*
+ * apply_row_security_relation
+ *
+ * It applies row-security policy on a particular relation being specified.
+ * If this relation is top of the inheritance tree, it also checks inherited
+ * children.
+ */
+static bool
+apply_row_security_relation(PlannerInfo *root, Index *vartrans,
+ CmdType cmd, Index rtindex)
+{
+ Query *parse = root->parse;
+ RangeTblEntry *rte = rt_fetch(rtindex, parse->rtable);
+ bool result = false;
+
+ if (!rte->inh)
+ {
+ Relation rel;
+ Expr *qual;
+ int flags = 0;
+
+ rel = heap_open(rte->relid, NoLock);
+ qual = pull_row_security_policy(cmd, rel, &flags);
+ if (qual)
+ {
+ vartrans[rtindex]
+ = expand_rtentry_with_policy(root, rtindex, qual, flags);
+ if (parse->resultRelation == rtindex)
+ parse->sourceRelation = vartrans[rtindex];
+ result = true;
+ }
+ heap_close(rel, NoLock);
+ }
+ else
+ {
+ ListCell *lc1, *lc2;
+
+ foreach (lc1, root->append_rel_list)
+ {
+ AppendRelInfo *apinfo = lfirst(lc1);
+
+ if (apinfo->parent_relid != rtindex)
+ continue;
+
+ if (apply_row_security_relation(root, vartrans, cmd,
+ apinfo->child_relid))
+ {
+ if (parse->resultRelation == rtindex)
+ apinfo->child_result = apinfo->child_relid;
+ apinfo->child_relid = vartrans[apinfo->child_relid];
+ foreach (lc2, apinfo->translated_vars)
+ {
+ Var *var = lfirst(lc2);
+
+ if (var)
+ var->varno = apinfo->child_relid;
+ }
+ result = true;
+ }
+ }
+ }
+ return result;
+}
+
+/*
+ * apply_row_security_recursive
+ *
+ * walker on join-tree
+ */
+static bool
+apply_row_security_recursive(PlannerInfo *root, Index *vartrans, Node *jtnode)
+{
+ bool result = false;
+
+ if (jtnode == NULL)
+ return false;
+ if (IsA(jtnode, RangeTblRef))
+ {
+ Index rtindex = ((RangeTblRef *) jtnode)->rtindex;
+ Query *parse = root->parse;
+ RangeTblEntry *rte = rt_fetch(rtindex, parse->rtable);
+ CmdType cmd;
+
+ /* Only relation can have row-security policy */
+ if (rte->rtekind != RTE_RELATION)
+ return false;
+
+ /*
+ * Prevents infinite recursion. Please note that rtindex == 1
+ * of the row-security subquery is a relation being already
+ * processed on the upper level.
+ */
+ if (parse->querySource == QSRC_ROW_SECURITY && rtindex == 1)
+ return false;
+
+ /* Is it a result relation of UPDATE or DELETE command? */
+ if (parse->resultRelation == rtindex)
+ cmd = parse->commandType;
+ else
+ cmd = CMD_SELECT;
+
+ /* Try to apply row-security policy, if configured */
+ result = apply_row_security_relation(root, vartrans, cmd, rtindex);
+ }
+ else if (IsA(jtnode, FromExpr))
+ {
+ FromExpr *f = (FromExpr *) jtnode;
+ ListCell *l;
+
+ foreach (l, f->fromlist)
+ {
+ if (apply_row_security_recursive(root, vartrans, lfirst(l)))
+ result = true;
+ }
+ }
+ else if (IsA(jtnode, JoinExpr))
+ {
+ JoinExpr *j = (JoinExpr *) jtnode;
+
+ if (apply_row_security_recursive(root, vartrans, j->larg))
+ result = true;
+ if (apply_row_security_recursive(root, vartrans, j->rarg))
+ result = true;
+ }
+ else
+ elog(ERROR, "unexpected node type: %d", (int) nodeTag(jtnode));
+
+ return result;
+}
+
+/*
+ * apply_row_security_policy
+ *
+ * Entrypoint to apply configured row-security policy of the relation.
+ *
+ * In case when the supplied query references relations with row-security
+ * policy, its RangeTblEntry shall be replaced by a row-security subquery
+ * that has simple scan on the referenced table with policy qualifiers.
+ * Of course, security-barrier shall be set on the subquery to prevent
+ * unexpected push-down of functions without leakproof flag.
+ *
+ * For example, when table t1 has a security policy "(x % 2 = 0)", the
+ * following query:
+ * SELECT * FROM t1 WHERE f_leak(y)
+ * performs as if
+ * SELECT * FROM (
+ * SELECT x, y FROM t1 WHERE (x % 2 = 0)
+ * ) AS t1 WHERE f_leak(y)
+ * would be given. Because the sub-query has security barrier flag,
+ * configured security policy qualifier is always executed prior to
+ * user given functions.
+ */
+void
+apply_row_security_policy(PlannerInfo *root)
+{
+ Query *parse = root->parse;
+ Oid curr_userid;
+ int curr_seccxt;
+ Index *vartrans;
+
+ /*
+ * Mode checks. In case when SECURITY_ROW_LEVEL_DISABLED is set,
+ * no row-level security policy should be applied regardless
+ * whether it is built-in or extension.
+ */
+ GetUserIdAndSecContext(&curr_userid, &curr_seccxt);
+ if (curr_seccxt & SECURITY_ROW_LEVEL_DISABLED)
+ return;
+
+ vartrans = palloc0(sizeof(Index) * (list_length(parse->rtable) + 1));
+ if (apply_row_security_recursive(root, vartrans, (Node *)parse->jointree))
+ {
+ PlannerGlobal *glob = root->glob;
+ PlanInvalItem *pi_item;
+ fixup_varnode_context context;
+
+ /*
+ * Constructed Plan with row-level security policy depends on
+ * properties of current user (database superuser can bypass
+ * configured row-security policy!), thus, it has to be
+ * invalidated when its assumption was changed.
+ */
+ if (!OidIsValid(glob->planUserId))
+ {
+ /* Plan invalidation on session user-id */
+ glob->planUserId = GetUserId();
+
+ /* Plan invalidation on catalog updates of pg_authid */
+ pi_item = makeNode(PlanInvalItem);
+ pi_item->cacheId = AUTHOID;
+ pi_item->hashValue =
+ GetSysCacheHashValue1(AUTHOID,
+ ObjectIdGetDatum(glob->planUserId));
+ glob->invalItems = lappend(glob->invalItems, pi_item);
+ }
+ else
+ Assert(glob->planUserId == GetUserId());
+
+ /*
+ * Var-nodes that referenced RangeTblEntry to be replaced by
+ * row-security sub-query have to be adjusted for appropriate
+ * reference to the underlying artificial column of the relation.
+ */
+ context.root = root;
+ context.varlevelsup = 0;
+ context.vartrans = vartrans;
+ query_tree_walker(parse,
+ fixup_varnode_walker,
+ (void *) &context,
+ QTW_IGNORE_RETURNING);
+ }
+ pfree(vartrans);
+}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index f67ef0c..af2ae94 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -256,6 +256,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
%type <node> alter_table_cmd alter_type_cmd opt_collate_clause
%type <list> alter_table_cmds alter_type_cmds
+%type <str> row_security_cmd
%type <dbehavior> opt_drop_behavior
@@ -2173,6 +2174,24 @@ alter_table_cmd:
n->def = (Node *)$2;
$$ = (Node *)n;
}
+ /* ALTER TABLE <name> SET ROW SECURITY FOR <cmd> TO (<expr>) */
+ | SET ROW SECURITY FOR row_security_cmd TO '(' a_expr ')'
+ {
+ AlterTableCmd *n = makeNode(AlterTableCmd);
+ n->subtype = AT_SetRowSecurity;
+ n->name = $5;
+ n->def = (Node *) $8;
+ $$ = (Node *)n;
+ }
+ /* ALTER TABLE <name> RESET ROW SECURITY FOR <cmd> */
+ | RESET ROW SECURITY FOR row_security_cmd
+ {
+ AlterTableCmd *n = makeNode(AlterTableCmd);
+ n->subtype = AT_ResetRowSecurity;
+ n->name = $5;
+ n->def = NULL;
+ $$ = (Node *)n;
+ }
| alter_generic_options
{
AlterTableCmd *n = makeNode(AlterTableCmd);
@@ -2244,6 +2263,12 @@ reloption_elem:
}
;
+row_security_cmd: ALL { $$ = "all"; }
+ | SELECT { $$ = "select"; }
+ | INSERT { $$ = "insert"; }
+ | UPDATE { $$ = "update"; }
+ | DELETE_P { $$ = "delete"; }
+ ;
/*****************************************************************************
*
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
index 7380618..14e02d4 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -269,6 +269,9 @@ transformAggregateCall(ParseState *pstate, Aggref *agg,
case EXPR_KIND_TRIGGER_WHEN:
err = _("aggregate functions are not allowed in trigger WHEN conditions");
break;
+ case EXPR_KIND_ROW_SECURITY:
+ err = _("aggregate functions are not allowed in row-security policy");
+ break;
/*
* There is intentionally no default: case here, so that the
@@ -537,6 +540,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
case EXPR_KIND_TRIGGER_WHEN:
err = _("window functions are not allowed in trigger WHEN conditions");
break;
+ case EXPR_KIND_ROW_SECURITY:
+ err = _("window functions are not allowed in row-security policy");
+ break;
/*
* There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 06f6512..0334ecb 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -1445,6 +1445,7 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
case EXPR_KIND_OFFSET:
case EXPR_KIND_RETURNING:
case EXPR_KIND_VALUES:
+ case EXPR_KIND_ROW_SECURITY:
/* okay */
break;
case EXPR_KIND_CHECK_CONSTRAINT:
@@ -2624,6 +2625,8 @@ ParseExprKindName(ParseExprKind exprKind)
return "EXECUTE";
case EXPR_KIND_TRIGGER_WHEN:
return "WHEN";
+ case EXPR_KIND_ROW_SECURITY:
+ return "ROW SECURITY";
/*
* There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c
index a9254c8..768d531 100644
--- a/src/backend/parser/parse_relation.c
+++ b/src/backend/parser/parse_relation.c
@@ -2058,6 +2058,25 @@ expandRelAttrs(ParseState *pstate, RangeTblEntry *rte,
}
/*
+ * getrelid
+ *
+ * Get OID of the relation corresponding to the given range index.
+ * Note that InvalidOid will be returned if the RTE is for neither
+ * relation nor sub-query originated from a relation
+ */
+Oid
+getrelid(Index rangeindex, List *rangetable)
+{
+ RangeTblEntry *rte = rt_fetch(rangeindex, rangetable);
+
+ if (rte->rtekind == RTE_RELATION)
+ return rte->relid;
+ if (rte->rtekind == RTE_SUBQUERY)
+ return rte->rowsec_relid;
+ return InvalidOid;
+}
+
+/*
* get_rte_attribute_name
* Get an attribute name from a RangeTblEntry
*
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index d909de3..1c8c984 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -3002,3 +3002,19 @@ QueryRewrite(Query *parsetree)
return results;
}
+
+/*
+ * QueryRewriteExpr
+ *
+ * This routine provides an entry point of query rewriter towards
+ * a certain expression tree with SubLink node; being added after
+ * the top level query rewrite.
+ * It primarily intends to expand views appeared in the qualifiers
+ * appended with row-level security which needs to modify query
+ * tree at head of the planner stage.
+ */
+void
+QueryRewriteExpr(Node *node, List *activeRIRs)
+{
+ fireRIRonSubLink(node, activeRIRs);
+}
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index 65edc1f..b36da97 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -3008,6 +3008,7 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
int spi_result;
Oid save_userid;
int save_sec_context;
+ int temp_sec_context;
Datum vals[RI_MAX_NUMKEYS * 2];
char nulls[RI_MAX_NUMKEYS * 2];
@@ -3087,8 +3088,18 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
/* Switch to proper UID to perform check as */
GetUserIdAndSecContext(&save_userid, &save_sec_context);
+
+ /*
+ * Row-level security should be disabled in case when foreign-key
+ * relation is queried to check existence of tuples that references
+ * the primary-key being modified.
+ */
+ temp_sec_context = save_sec_context | SECURITY_LOCAL_USERID_CHANGE;
+ if (source_is_pk)
+ temp_sec_context |= SECURITY_ROW_LEVEL_DISABLED;
+
SetUserIdAndSecContext(RelationGetForm(query_rel)->relowner,
- save_sec_context | SECURITY_LOCAL_USERID_CHANGE);
+ temp_sec_context);
/* Finally we can run the query. */
spi_result = SPI_execute_snapshot(qplan,
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index 26cae97..d05cde7 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -53,6 +53,7 @@
#include "catalog/namespace.h"
#include "executor/executor.h"
#include "executor/spi.h"
+#include "miscadmin.h"
#include "nodes/nodeFuncs.h"
#include "optimizer/planmain.h"
#include "optimizer/prep.h"
@@ -794,6 +795,16 @@ CheckCachedPlan(CachedPlanSource *plansource)
AcquireExecutorLocks(plan->stmt_list, true);
/*
+ * If plan was constructed with assumption of a particular user-id,
+ * and it is different from the current one, the cached-plan shall
+ * be invalidated to construct suitable query plan.
+ */
+ if (plan->is_valid &&
+ OidIsValid(plan->planUserId) &&
+ plan->planUserId == GetUserId())
+ plan->is_valid = false;
+
+ /*
* If plan was transient, check to see if TransactionXmin has
* advanced, and if so invalidate it.
*/
@@ -846,6 +857,8 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
{
CachedPlan *plan;
List *plist;
+ ListCell *cell;
+ Oid planUserId = InvalidOid;
bool snapshot_set;
bool spi_pushed;
MemoryContext plan_context;
@@ -913,6 +926,24 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
PopActiveSnapshot();
/*
+ * Check whether the generated plan assumes a particular user-id, or not.
+ * In case when a valid user-id is recorded on PlannedStmt->planUserId,
+ * it should be kept and used to validation check of the cached plan
+ * under the "current" user-id.
+ */
+ foreach (cell, plist)
+ {
+ PlannedStmt *pstmt = lfirst(cell);
+
+ if (IsA(pstmt, PlannedStmt) && OidIsValid(pstmt->planUserId))
+ {
+ Assert(!OidIsValid(planUserId) || planUserId == pstmt->planUserId);
+
+ planUserId = pstmt->planUserId;
+ }
+ }
+
+ /*
* Normally we make a dedicated memory context for the CachedPlan and its
* subsidiary data. (It's probably not going to be large, but just in
* case, use the default maxsize parameter. It's transient for the
@@ -955,6 +986,7 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
plan->is_oneshot = plansource->is_oneshot;
plan->is_saved = false;
plan->is_valid = true;
+ plan->planUserId = planUserId;
/* assign generation number to new plan */
plan->generation = ++(plansource->generation);
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 66fb63b..828c10e 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -50,6 +50,7 @@
#include "catalog/pg_opclass.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_rewrite.h"
+#include "catalog/pg_rowsecurity.h"
#include "catalog/pg_tablespace.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
@@ -935,6 +936,11 @@ RelationBuildDesc(Oid targetRelId, bool insertIt)
else
relation->trigdesc = NULL;
+ if (relation->rd_rel->relhasrowsecurity)
+ RelationBuildRowSecurity(relation);
+ else
+ relation->rsdesc = NULL;
+
/*
* if it's an index, initialize index-related information
*/
@@ -1842,6 +1848,8 @@ RelationDestroyRelation(Relation relation)
MemoryContextDelete(relation->rd_indexcxt);
if (relation->rd_rulescxt)
MemoryContextDelete(relation->rd_rulescxt);
+ if (relation->rsdesc)
+ MemoryContextDelete(relation->rsdesc->rscxt);
if (relation->rd_fdwroutine)
pfree(relation->rd_fdwroutine);
pfree(relation);
@@ -3161,7 +3169,13 @@ RelationCacheInitializePhase3(void)
relation->rd_rel->relhastriggers = false;
restart = true;
}
-
+ if (relation->rd_rel->relhasrowsecurity && relation->rsdesc == NULL)
+ {
+ RelationBuildRowSecurity(relation);
+ if (relation->rsdesc == NULL)
+ relation->rd_rel->relhasrowsecurity = false;
+ restart = true;
+ }
/* Release hold on the relation */
RelationDecrementReferenceCount(relation);
@@ -4407,6 +4421,7 @@ load_relcache_init_file(bool shared)
rel->rd_rules = NULL;
rel->rd_rulescxt = NULL;
rel->trigdesc = NULL;
+ rel->rsdesc = NULL;
rel->rd_indexprs = NIL;
rel->rd_indpred = NIL;
rel->rd_exclops = NULL;
diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c
index 247ad92..9603ef4 100644
--- a/src/bin/pg_dump/common.c
+++ b/src/bin/pg_dump/common.c
@@ -244,6 +244,10 @@ getSchemaData(Archive *fout, int *numTablesPtr)
write_msg(NULL, "reading rewrite rules\n");
getRules(fout, &numRules);
+ if (g_verbose)
+ write_msg(NULL, "reading row-security policies\n");
+ getRowSecurity(fout, tblinfo, numTables);
+
*numTablesPtr = numTables;
return tblinfo;
}
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index cd7669b..e9ca251 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3148,6 +3148,7 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, RestoreOptions *ropt, bool isDat
strcmp(te->desc, "INDEX") == 0 ||
strcmp(te->desc, "RULE") == 0 ||
strcmp(te->desc, "TRIGGER") == 0 ||
+ strcmp(te->desc, "ROW SECURITY") == 0 ||
strcmp(te->desc, "USER MAPPING") == 0)
{
/* these object types don't have separate owners */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index f40961f..fb15f6f 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -249,6 +249,7 @@ static char *myFormatType(const char *typname, int32 typmod);
static void getBlobs(Archive *fout);
static void dumpBlob(Archive *fout, BlobInfo *binfo);
static int dumpBlobs(Archive *fout, void *arg);
+static void dumpRowSecurity(Archive *fout, RowSecurityInfo *rsinfo);
static void dumpDatabase(Archive *AH);
static void dumpEncoding(Archive *AH);
static void dumpStdStrings(Archive *AH);
@@ -2686,6 +2687,134 @@ dumpBlobs(Archive *fout, void *arg)
return 1;
}
+/*
+ * getRowSecurity
+ * get information about every row-security policy on a dumpable table
+ */
+void
+getRowSecurity(Archive *fout, TableInfo tblinfo[], int numTables)
+{
+ PQExpBuffer query = createPQExpBuffer();
+ PGresult *res;
+ RowSecurityInfo *rsinfo;
+ int i_oid;
+ int i_tableoid;
+ int i_rseccmd;
+ int i_rsecqual;
+ int i, j, ntups;
+
+ /* row-security is not supported prior to v9.4 */
+ if (fout->remoteVersion < 90400)
+ return;
+
+ for (i=0; i < numTables; i++)
+ {
+ TableInfo *tbinfo = &tblinfo[i];
+
+ if (!tbinfo->hasrowsec || !tbinfo->dobj.dump)
+ continue;
+
+ if (g_verbose)
+ write_msg(NULL, "reading row-security policy for table \"%s\"\n",
+ tbinfo->dobj.name);
+
+ /*
+ * select table schema to ensure regproc name is qualified if needed
+ */
+ selectSourceSchema(fout, tbinfo->dobj.namespace->dobj.name);
+
+ resetPQExpBuffer(query);
+
+ appendPQExpBuffer(query,
+ "SELECT oid, tableoid, s.rseccmd, "
+ "pg_get_expr(s.rsecqual, s.rsecrelid) AS rsecqual "
+ "FROM pg_catalog.pg_rowsecurity s "
+ "WHERE rsecrelid = '%u'",
+ tbinfo->dobj.catId.oid);
+ res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+
+ ntups = PQntuples(res);
+
+ i_oid = PQfnumber(res, "oid");
+ i_tableoid = PQfnumber(res, "tableoid");
+ i_rseccmd = PQfnumber(res, "rseccmd");
+ i_rsecqual = PQfnumber(res, "rsecqual");
+
+ rsinfo = pg_malloc(ntups * sizeof(RowSecurityInfo));
+ for (j=0; j < ntups; j++)
+ {
+ char namebuf[NAMEDATALEN + 1];
+
+ rsinfo[j].dobj.objType = DO_ROW_SECURITY;
+ rsinfo[j].dobj.catId.tableoid =
+ atooid(PQgetvalue(res, j, i_tableoid));
+ rsinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
+ AssignDumpId(&rsinfo[j].dobj);
+ snprintf(namebuf, sizeof(namebuf), "row-security of %s",
+ tbinfo->rolname);
+ rsinfo[j].dobj.name = namebuf;
+ rsinfo[j].dobj.namespace = tbinfo->dobj.namespace;
+ rsinfo[j].rstable = tbinfo;
+ rsinfo[j].rseccmd = pg_strdup(PQgetvalue(res, j, i_rseccmd));
+ rsinfo[j].rsecqual = pg_strdup(PQgetvalue(res, j, i_rsecqual));
+ }
+ PQclear(res);
+ }
+ destroyPQExpBuffer(query);
+}
+
+/*
+ * dumpRowSecurity
+ * dump the definition of the given row-security policy
+ */
+static void
+dumpRowSecurity(Archive *fout, RowSecurityInfo *rsinfo)
+{
+ TableInfo *tbinfo = rsinfo->rstable;
+ PQExpBuffer query;
+ PQExpBuffer delqry;
+ const char *cmd;
+
+ if (dataOnly || !tbinfo->hasrowsec)
+ return;
+
+ query = createPQExpBuffer();
+ delqry = createPQExpBuffer();
+ appendPQExpBuffer(query, "ALTER TABLE %s SET ROW SECURITY ",
+ fmtId(tbinfo->dobj.name));
+ appendPQExpBuffer(delqry, "ALTER TABLE %s RESET ROW SECURITY ",
+ fmtId(tbinfo->dobj.name));
+ if (strcmp(rsinfo->rseccmd, "a") == 0)
+ cmd = "ALL";
+ else if (strcmp(rsinfo->rseccmd, "s") == 0)
+ cmd = "SELECT";
+ else if (strcmp(rsinfo->rseccmd, "i") == 0)
+ cmd = "INSERT";
+ else if (strcmp(rsinfo->rseccmd, "u") == 0)
+ cmd = "UPDATE";
+ else if (strcmp(rsinfo->rseccmd, "d") == 0)
+ cmd = "DELETE";
+ else
+ {
+ write_msg(NULL, "unexpected command type: '%s'\n", rsinfo->rseccmd);
+ exit_nicely(1);
+ }
+ appendPQExpBuffer(query, "FOR %s TO %s;\n", cmd, rsinfo->rsecqual);
+ appendPQExpBuffer(delqry, "FOR %s;\n", cmd);
+
+ ArchiveEntry(fout, rsinfo->dobj.catId, rsinfo->dobj.dumpId,
+ rsinfo->dobj.name,
+ rsinfo->dobj.namespace->dobj.name,
+ NULL,
+ tbinfo->rolname, false,
+ "ROW SECURITY", SECTION_POST_DATA,
+ query->data, delqry->data, NULL,
+ NULL, 0,
+ NULL, NULL);
+
+ destroyPQExpBuffer(query);
+}
+
static void
binary_upgrade_set_type_oids_by_type_oid(Archive *fout,
PQExpBuffer upgrade_buffer,
@@ -4214,6 +4343,7 @@ getTables(Archive *fout, int *numTables)
int i_relhastriggers;
int i_relhasindex;
int i_relhasrules;
+ int i_relhasrowsec;
int i_relhasoids;
int i_relfrozenxid;
int i_toastoid;
@@ -4251,7 +4381,45 @@ getTables(Archive *fout, int *numTables)
* we cannot correctly identify inherited columns, owned sequences, etc.
*/
- if (fout->remoteVersion >= 90300)
+ if (fout->remoteVersion >= 90400)
+ {
+ /*
+ * Left join to pick up dependency info linking sequences to their
+ * owning column, if any (note this dependency is AUTO as of 8.2)
+ */
+ appendPQExpBuffer(query,
+ "SELECT c.tableoid, c.oid, c.relname, "
+ "c.relacl, c.relkind, c.relnamespace, "
+ "(%s c.relowner) AS rolname, "
+ "c.relchecks, c.relhastriggers, "
+ "c.relhasindex, c.relhasrules, c.relhasoids, "
+ "c.relhasrowsecurity, "
+ "c.relfrozenxid, tc.oid AS toid, "
+ "tc.relfrozenxid AS tfrozenxid, "
+ "c.relpersistence, c.relispopulated, "
+ "c.relpages, "
+ "CASE WHEN c.reloftype <> 0 THEN c.reloftype::pg_catalog.regtype ELSE NULL END AS reloftype, "
+ "d.refobjid AS owning_tab, "
+ "d.refobjsubid AS owning_col, "
+ "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
+ "array_to_string(c.reloptions, ', ') AS reloptions, "
+ "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions "
+ "FROM pg_class c "
+ "LEFT JOIN pg_depend d ON "
+ "(c.relkind = '%c' AND "
+ "d.classid = c.tableoid AND d.objid = c.oid AND "
+ "d.objsubid = 0 AND "
+ "d.refclassid = c.tableoid AND d.deptype = 'a') "
+ "LEFT JOIN pg_class tc ON (c.reltoastrelid = tc.oid) "
+ "WHERE c.relkind in ('%c', '%c', '%c', '%c', '%c', '%c') "
+ "ORDER BY c.oid",
+ username_subquery,
+ RELKIND_SEQUENCE,
+ RELKIND_RELATION, RELKIND_SEQUENCE,
+ RELKIND_VIEW, RELKIND_COMPOSITE_TYPE,
+ RELKIND_MATVIEW, RELKIND_FOREIGN_TABLE);
+ }
+ else if (fout->remoteVersion >= 90300)
{
/*
* Left join to pick up dependency info linking sequences to their
@@ -4263,6 +4431,7 @@ getTables(Archive *fout, int *numTables)
"(%s c.relowner) AS rolname, "
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"c.relfrozenxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"c.relpersistence, c.relispopulated, "
@@ -4300,6 +4469,7 @@ getTables(Archive *fout, int *numTables)
"(%s c.relowner) AS rolname, "
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"c.relfrozenxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"c.relpersistence, 't' as relispopulated, "
@@ -4337,6 +4507,7 @@ getTables(Archive *fout, int *numTables)
"(%s c.relowner) AS rolname, "
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"c.relfrozenxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"'p' AS relpersistence, 't' as relispopulated, "
@@ -4373,6 +4544,7 @@ getTables(Archive *fout, int *numTables)
"(%s c.relowner) AS rolname, "
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"c.relfrozenxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"'p' AS relpersistence, 't' as relispopulated, "
@@ -4409,6 +4581,7 @@ getTables(Archive *fout, int *numTables)
"(%s c.relowner) AS rolname, "
"c.relchecks, (c.reltriggers <> 0) AS relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"c.relfrozenxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"'p' AS relpersistence, 't' as relispopulated, "
@@ -4445,6 +4618,7 @@ getTables(Archive *fout, int *numTables)
"(%s relowner) AS rolname, "
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"0 AS relfrozenxid, "
"0 AS toid, "
"0 AS tfrozenxid, "
@@ -4481,6 +4655,7 @@ getTables(Archive *fout, int *numTables)
"(%s relowner) AS rolname, "
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"0 AS relfrozenxid, "
"0 AS toid, "
"0 AS tfrozenxid, "
@@ -4513,6 +4688,7 @@ getTables(Archive *fout, int *numTables)
"(%s relowner) AS rolname, "
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"0 AS relfrozenxid, "
"0 AS toid, "
"0 AS tfrozenxid, "
@@ -4540,6 +4716,7 @@ getTables(Archive *fout, int *numTables)
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, "
"'t'::bool AS relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"0 AS relfrozenxid, "
"0 AS toid, "
"0 AS tfrozenxid, "
@@ -4577,6 +4754,7 @@ getTables(Archive *fout, int *numTables)
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, "
"'t'::bool AS relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"0 as relfrozenxid, "
"0 AS toid, "
"0 AS tfrozenxid, "
@@ -4624,6 +4802,7 @@ getTables(Archive *fout, int *numTables)
i_relhastriggers = PQfnumber(res, "relhastriggers");
i_relhasindex = PQfnumber(res, "relhasindex");
i_relhasrules = PQfnumber(res, "relhasrules");
+ i_relhasrowsec = PQfnumber(res, "relhasrowsecurity");
i_relhasoids = PQfnumber(res, "relhasoids");
i_relfrozenxid = PQfnumber(res, "relfrozenxid");
i_toastoid = PQfnumber(res, "toid");
@@ -4671,6 +4850,7 @@ getTables(Archive *fout, int *numTables)
tblinfo[i].hasindex = (strcmp(PQgetvalue(res, i, i_relhasindex), "t") == 0);
tblinfo[i].hasrules = (strcmp(PQgetvalue(res, i, i_relhasrules), "t") == 0);
tblinfo[i].hastriggers = (strcmp(PQgetvalue(res, i, i_relhastriggers), "t") == 0);
+ tblinfo[i].hasrowsec = (strcmp(PQgetvalue(res, i, i_relhasrowsec), "t") == 0);
tblinfo[i].hasoids = (strcmp(PQgetvalue(res, i, i_relhasoids), "t") == 0);
tblinfo[i].relispopulated = (strcmp(PQgetvalue(res, i, i_relispopulated), "t") == 0);
tblinfo[i].relpages = atoi(PQgetvalue(res, i, i_relpages));
@@ -7748,6 +7928,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
NULL, 0,
dumpBlobs, NULL);
break;
+ case DO_ROW_SECURITY:
+ dumpRowSecurity(fout, (RowSecurityInfo *) dobj);
+ break;
case DO_PRE_DATA_BOUNDARY:
case DO_POST_DATA_BOUNDARY:
/* never dumped, nothing to do */
@@ -14922,6 +15105,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
case DO_TRIGGER:
case DO_EVENT_TRIGGER:
case DO_DEFAULT_ACL:
+ case DO_ROW_SECURITY:
/* Post-data objects: must come after the post-data boundary */
addObjectDependency(dobj, postDataBound->dumpId);
break;
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 5582538..31e009a 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -111,7 +111,8 @@ typedef enum
DO_PRE_DATA_BOUNDARY,
DO_POST_DATA_BOUNDARY,
DO_EVENT_TRIGGER,
- DO_REFRESH_MATVIEW
+ DO_REFRESH_MATVIEW,
+ DO_ROW_SECURITY,
} DumpableObjectType;
typedef struct _dumpableObject
@@ -243,6 +244,7 @@ typedef struct _tableInfo
bool hasindex; /* does it have any indexes? */
bool hasrules; /* does it have any rules? */
bool hastriggers; /* does it have any triggers? */
+ bool hasrowsec; /* does it have any row-security policy? */
bool hasoids; /* does it have OIDs? */
uint32 frozenxid; /* for restore frozen xid */
Oid toast_oid; /* for restore toast frozen xid */
@@ -481,6 +483,14 @@ typedef struct _blobInfo
char *blobacl;
} BlobInfo;
+typedef struct _rowSecurityInfo
+{
+ DumpableObject dobj;
+ TableInfo *rstable;
+ char *rseccmd;
+ char *rsecqual;
+} RowSecurityInfo;
+
/* global decls */
extern bool force_quotes; /* double-quotes for identifiers flag */
extern bool g_verbose; /* verbose flag */
@@ -572,5 +582,6 @@ extern DefaultACLInfo *getDefaultACLs(Archive *fout, int *numDefaultACLs);
extern void getExtensionMembership(Archive *fout, ExtensionInfo extinfo[],
int numExtensions);
extern EventTriggerInfo *getEventTriggers(Archive *fout, int *numEventTriggers);
+extern void getRowSecurity(Archive *fout, TableInfo tblinfo[], int numTables);
#endif /* PG_DUMP_H */
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 141e713..5c5777c 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -1342,6 +1342,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
"BLOB DATA (ID %d)",
obj->dumpId);
return;
+ case DO_ROW_SECURITY:
+ snprintf(buf, bufsize,
+ "ROW-SECURITY POLICY (ID %d OID %u)",
+ obj->dumpId, obj->catId.oid);
+ return;
case DO_PRE_DATA_BOUNDARY:
snprintf(buf, bufsize,
"PRE-DATA BOUNDARY (ID %d)",
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 9b6b9c2..db5c0e5 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2698,6 +2698,10 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys
appendPQExpBuffer(&buf,
",\n pg_catalog.obj_description(c.oid, 'pg_class') as \"%s\"",
gettext_noop("Description"));
+ if (pset.sversion >= 90300)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_get_expr(rs.rsecqual, c.oid) as \"%s\"",
+ gettext_noop("Row-security"));
}
appendPQExpBuffer(&buf,
@@ -2707,6 +2711,9 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys
appendPQExpBuffer(&buf,
"\n LEFT JOIN pg_catalog.pg_index i ON i.indexrelid = c.oid"
"\n LEFT JOIN pg_catalog.pg_class c2 ON i.indrelid = c2.oid");
+ if (verbose && pset.sversion >= 90300)
+ appendPQExpBuffer(&buf,
+ "\n LEFT JOIN pg_rowsecurity rs ON rs.rsecrelid = c.oid");
appendPQExpBuffer(&buf, "\nWHERE c.relkind IN (");
if (showTables)
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index 3aefbb5e..c96e8d4 100644
--- a/src/include/catalog/dependency.h
+++ b/src/include/catalog/dependency.h
@@ -147,6 +147,7 @@ typedef enum ObjectClass
OCLASS_DEFACL, /* pg_default_acl */
OCLASS_EXTENSION, /* pg_extension */
OCLASS_EVENT_TRIGGER, /* pg_event_trigger */
+ OCLASS_ROWSECURITY, /* pg_rowsecurity */
MAX_OCLASS /* MUST BE LAST */
} ObjectClass;
diff --git a/src/include/catalog/indexing.h b/src/include/catalog/indexing.h
index 19268fb..331a64b 100644
--- a/src/include/catalog/indexing.h
+++ b/src/include/catalog/indexing.h
@@ -311,6 +311,11 @@ DECLARE_UNIQUE_INDEX(pg_extension_name_index, 3081, on pg_extension using btree(
DECLARE_UNIQUE_INDEX(pg_range_rngtypid_index, 3542, on pg_range using btree(rngtypid oid_ops));
#define RangeTypidIndexId 3542
+DECLARE_UNIQUE_INDEX(pg_rowsecurity_oid_index, 3819, on pg_rowsecurity using btree(oid oid_ops));
+#define RowSecurityOidIndexId 3819
+DECLARE_UNIQUE_INDEX(pg_rowsecurity_relid_index, 3839, on pg_rowsecurity using btree(rsecrelid oid_ops, rseccmd char_ops));
+#define RowSecurityRelidIndexId 3839
+
/* last step of initialization script: build the indexes declared above */
BUILD_INDICES
diff --git a/src/include/catalog/pg_class.h b/src/include/catalog/pg_class.h
index 49c4f6f..6a16819 100644
--- a/src/include/catalog/pg_class.h
+++ b/src/include/catalog/pg_class.h
@@ -64,6 +64,7 @@ CATALOG(pg_class,1259) BKI_BOOTSTRAP BKI_ROWTYPE_OID(83) BKI_SCHEMA_MACRO
bool relhaspkey; /* has (or has had) PRIMARY KEY index */
bool relhasrules; /* has (or has had) any rules */
bool relhastriggers; /* has (or has had) any TRIGGERs */
+ bool relhasrowsecurity; /* has (or has had) row-security policy */
bool relhassubclass; /* has (or has had) derived classes */
bool relispopulated; /* matview currently holds query results */
TransactionId relfrozenxid; /* all Xids < this are frozen in this rel */
@@ -93,7 +94,7 @@ typedef FormData_pg_class *Form_pg_class;
* ----------------
*/
-#define Natts_pg_class 28
+#define Natts_pg_class 29
#define Anum_pg_class_relname 1
#define Anum_pg_class_relnamespace 2
#define Anum_pg_class_reltype 3
@@ -116,12 +117,13 @@ typedef FormData_pg_class *Form_pg_class;
#define Anum_pg_class_relhaspkey 20
#define Anum_pg_class_relhasrules 21
#define Anum_pg_class_relhastriggers 22
-#define Anum_pg_class_relhassubclass 23
-#define Anum_pg_class_relispopulated 24
-#define Anum_pg_class_relfrozenxid 25
-#define Anum_pg_class_relminmxid 26
-#define Anum_pg_class_relacl 27
-#define Anum_pg_class_reloptions 28
+#define Anum_pg_class_relhasrowsecurity 23
+#define Anum_pg_class_relhassubclass 24
+#define Anum_pg_class_relispopulated 25
+#define Anum_pg_class_relfrozenxid 26
+#define Anum_pg_class_relminmxid 27
+#define Anum_pg_class_relacl 28
+#define Anum_pg_class_reloptions 29
/* ----------------
* initial contents of pg_class
@@ -136,13 +138,13 @@ typedef FormData_pg_class *Form_pg_class;
* Note: "3" in the relfrozenxid column stands for FirstNormalTransactionId;
* similarly, "1" in relminmxid stands for FirstMultiXactId
*/
-DATA(insert OID = 1247 ( pg_type PGNSP 71 0 PGUID 0 0 0 0 0 0 0 f f p r 30 0 t f f f f t 3 1 _null_ _null_ ));
+DATA(insert OID = 1247 ( pg_type PGNSP 71 0 PGUID 0 0 0 0 0 0 0 f f p r 30 0 t f f f f f t 3 1 _null_ _null_ ));
DESCR("");
-DATA(insert OID = 1249 ( pg_attribute PGNSP 75 0 PGUID 0 0 0 0 0 0 0 f f p r 21 0 f f f f f t 3 1 _null_ _null_ ));
+DATA(insert OID = 1249 ( pg_attribute PGNSP 75 0 PGUID 0 0 0 0 0 0 0 f f p r 21 0 f f f f f f t 3 1 _null_ _null_ ));
DESCR("");
-DATA(insert OID = 1255 ( pg_proc PGNSP 81 0 PGUID 0 0 0 0 0 0 0 f f p r 27 0 t f f f f t 3 1 _null_ _null_ ));
+DATA(insert OID = 1255 ( pg_proc PGNSP 81 0 PGUID 0 0 0 0 0 0 0 f f p r 27 0 t f f f f f t 3 1 _null_ _null_ ));
DESCR("");
-DATA(insert OID = 1259 ( pg_class PGNSP 83 0 PGUID 0 0 0 0 0 0 0 f f p r 28 0 t f f f f t 3 1 _null_ _null_ ));
+DATA(insert OID = 1259 ( pg_class PGNSP 83 0 PGUID 0 0 0 0 0 0 0 f f p r 29 0 t f f f f f t 3 1 _null_ _null_ ));
DESCR("");
diff --git a/src/include/catalog/pg_rowsecurity.h b/src/include/catalog/pg_rowsecurity.h
new file mode 100644
index 0000000..a068b35
--- /dev/null
+++ b/src/include/catalog/pg_rowsecurity.h
@@ -0,0 +1,76 @@
+/*
+ * pg_rowsecurity.h
+ * definition of the system catalog for row-security policy (pg_rowsecurity)
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ */
+#ifndef PG_ROWSECURITY_H
+#define PG_ROWSECURITY_H
+
+#include "catalog/genbki.h"
+#include "nodes/primnodes.h"
+#include "utils/memutils.h"
+#include "utils/relcache.h"
+
+/* ----------------
+ * pg_rowlevelsec definition. cpp turns this into
+ * typedef struct FormData_pg_rowlevelsec
+ * ----------------
+ */
+#define RowSecurityRelationId 3838
+
+CATALOG(pg_rowsecurity,3838)
+{
+ /* Oid of the relation that has row-security policy */
+ Oid rsecrelid;
+
+ /* One of ROWSECURITY_CMD_* below */
+ char rseccmd;
+#ifdef CATALOG_VARLEN
+ pg_node_tree rsecqual;
+#endif
+} FormData_pg_rowsecurity;
+
+/* ----------------
+ * Form_pg_rowlevelsec corresponds to a pointer to a row with
+ * the format of pg_rowlevelsec relation.
+ * ----------------
+ */
+typedef FormData_pg_rowsecurity *Form_pg_rowsecurity;
+
+/* ----------------
+ * compiler constants for pg_rowlevelsec
+ * ----------------
+ */
+#define Natts_pg_rowsecurity 3
+#define Anum_pg_rowsecurity_rsecrelid 1
+#define Anum_pg_rowsecurity_rseccmd 2
+#define Anum_pg_rowsecurity_rsecqual 3
+
+#define ROWSECURITY_CMD_ALL 'a'
+#define ROWSECURITY_CMD_SELECT 's'
+#define ROWSECURITY_CMD_INSERT 'i'
+#define ROWSECURITY_CMD_UPDATE 'u'
+#define ROWSECURITY_CMD_DELETE 'd'
+
+typedef struct
+{
+ Oid rsecid;
+ Expr *qual;
+ bool hassublinks;
+} RowSecurityEntry;
+
+typedef struct
+{
+ MemoryContext rscxt;
+ RowSecurityEntry rsall; /* row-security policy for ALL */
+} RowSecurityDesc;
+
+extern void RelationBuildRowSecurity(Relation relation);
+extern void ATExecSetRowSecurity(Relation relation,
+ const char *cmdname, Node *clause);
+extern void RemoveRowSecurityById(Oid relationId);
+
+#endif /* PG_ROWSECURITY_H */
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 48985b3..c182d2d 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -279,6 +279,7 @@ extern int trace_recovery(int trace_level);
/* flags to be OR'd to form sec_context */
#define SECURITY_LOCAL_USERID_CHANGE 0x0001
#define SECURITY_RESTRICTED_OPERATION 0x0002
+#define SECURITY_ROW_LEVEL_DISABLED 0x0004
extern char *DatabasePath;
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 4f77016..f49cbf9 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -306,6 +306,8 @@ typedef struct JunkFilter
* ConstraintExprs array of constraint-checking expr states
* junkFilter for removing junk attributes from tuples
* projectReturning for computing a RETURNING list
+ * rowSecurity for row-security checks
+ * rowSecParams param-list if row-security has SubLink
* ----------------
*/
typedef struct ResultRelInfo
@@ -325,6 +327,8 @@ typedef struct ResultRelInfo
List **ri_ConstraintExprs;
JunkFilter *ri_junkFilter;
ProjectionInfo *ri_projectReturning;
+ Node *ri_rowSecurity;
+ List *ri_rowSecParams;
} ResultRelInfo;
/* ----------------
diff --git a/src/include/nodes/nodeFuncs.h b/src/include/nodes/nodeFuncs.h
index d4901ca..cb30110 100644
--- a/src/include/nodes/nodeFuncs.h
+++ b/src/include/nodes/nodeFuncs.h
@@ -24,6 +24,7 @@
#define QTW_IGNORE_RANGE_TABLE 0x08 /* skip rangetable entirely */
#define QTW_EXAMINE_RTES 0x10 /* examine RTEs */
#define QTW_DONT_COPY_QUERY 0x20 /* do not copy top Query */
+#define QTW_IGNORE_RETURNING 0x40 /* skip returning clause */
extern Oid exprType(const Node *expr);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index de22dff..2b81e27 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -31,7 +31,8 @@ typedef enum QuerySource
QSRC_PARSER, /* added by parse analysis (now unused) */
QSRC_INSTEAD_RULE, /* added by unconditional INSTEAD rule */
QSRC_QUAL_INSTEAD_RULE, /* added by conditional INSTEAD rule */
- QSRC_NON_INSTEAD_RULE /* added by non-INSTEAD rule */
+ QSRC_NON_INSTEAD_RULE, /* added by non-INSTEAD rule */
+ QSRC_ROW_SECURITY, /* added by row-security */
} QuerySource;
/* Sort ordering options for ORDER BY and CREATE INDEX */
@@ -112,7 +113,9 @@ typedef struct Query
int resultRelation; /* rtable index of target relation for
* INSERT/UPDATE/DELETE; 0 for SELECT */
-
+ int sourceRelation; /* rtable index of source relation for
+ * UPDATE/DELETE, if not identical with
+ * resultRelation; 0 for elsewhere */
bool hasAggs; /* has aggregates in tlist or havingQual */
bool hasWindowFuncs; /* has window functions in tlist */
bool hasSubLinks; /* has subquery SubLink */
@@ -724,6 +727,11 @@ typedef struct RangeTblEntry
*/
Query *subquery; /* the sub-query */
bool security_barrier; /* is from security_barrier view? */
+ Oid rowsec_relid; /* OID of the original relation, if this
+ * sub-query originated from row-security
+ * policy on the relation. Elsewhere, it
+ * should be InvalidOid.
+ */
/*
* Fields valid for a join RTE (else NULL/zero):
@@ -1250,6 +1258,8 @@ typedef enum AlterTableType
AT_DropInherit, /* NO INHERIT parent */
AT_AddOf, /* OF <type_name> */
AT_DropOf, /* NOT OF */
+ AT_SetRowSecurity, /* SET ROW SECURITY (...) */
+ AT_ResetRowSecurity, /* RESET ROW SECURITY */
AT_GenericOptions /* OPTIONS (...) */
} AlterTableType;
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 841701e..cc94421 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -67,6 +67,8 @@ typedef struct PlannedStmt
List *invalItems; /* other dependencies, as PlanInvalItems */
int nParamExec; /* number of PARAM_EXEC Params used */
+
+ Oid planUserId; /* user-id this plan assumed, or InvalidOid */
} PlannedStmt;
/* macro for fetching the Plan associated with a SubPlan node */
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index c0a636b..ed01b65 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -98,6 +98,8 @@ typedef struct PlannerGlobal
Index lastRowMarkId; /* highest PlanRowMark ID assigned */
bool transientPlan; /* redo plan when TransactionXmin changes? */
+
+ Oid planUserId; /* User-Id to be assumed on this plan */
} PlannerGlobal;
/* macro for fetching the Plan associated with a SubPlan node */
@@ -1416,6 +1418,10 @@ typedef struct AppendRelInfo
*/
Index parent_relid; /* RT index of append parent rel */
Index child_relid; /* RT index of append child rel */
+ Index child_result; /* RT index of append child rel's source,
+ * if source of result relation is not
+ * identical. Elsewhere, 0.
+ */
/*
* For an inheritance appendrel, the parent and child are both regular
diff --git a/src/include/optimizer/rowsecurity.h b/src/include/optimizer/rowsecurity.h
new file mode 100644
index 0000000..ff4dd9a
--- /dev/null
+++ b/src/include/optimizer/rowsecurity.h
@@ -0,0 +1,27 @@
+/* -------------------------------------------------------------------------
+ *
+ * rowsecurity.h
+ * prototypes for optimizer/rowsecurity.c
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * -------------------------------------------------------------------------
+ */
+#ifndef ROWSECURITY_H
+#define ROWSECURITY_H
+
+#include "nodes/execnodes.h"
+#include "nodes/parsenodes.h"
+#include "nodes/relation.h"
+#include "utils/rel.h"
+
+typedef List *(*row_security_policy_hook_type)(CmdType cmdtype,
+ Relation relation);
+extern PGDLLIMPORT row_security_policy_hook_type row_security_policy_hook;
+
+extern bool copy_row_security_policy(CopyStmt *stmt,
+ Relation relation, List *attnums);
+extern void apply_row_security_policy(PlannerInfo *root);
+
+#endif /* ROWSECURITY_H */
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index 49ca764..a3d5be1 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -62,7 +62,8 @@ typedef enum ParseExprKind
EXPR_KIND_INDEX_PREDICATE, /* index predicate */
EXPR_KIND_ALTER_COL_TRANSFORM, /* transform expr in ALTER COLUMN TYPE */
EXPR_KIND_EXECUTE_PARAMETER, /* parameter value in EXECUTE */
- EXPR_KIND_TRIGGER_WHEN /* WHEN condition in CREATE TRIGGER */
+ EXPR_KIND_TRIGGER_WHEN, /* WHEN condition in CREATE TRIGGER */
+ EXPR_KIND_ROW_SECURITY, /* ROW SECURITY policy for a table */
} ParseExprKind;
diff --git a/src/include/parser/parsetree.h b/src/include/parser/parsetree.h
index 9608cc6..b4e9df4 100644
--- a/src/include/parser/parsetree.h
+++ b/src/include/parser/parsetree.h
@@ -32,14 +32,11 @@
((RangeTblEntry *) list_nth(rangetable, (rangetable_index)-1))
/*
- * getrelid
- *
* Given the range index of a relation, return the corresponding
* relation OID. Note that InvalidOid will be returned if the
* RTE is for a non-relation-type RTE.
*/
-#define getrelid(rangeindex,rangetable) \
- (rt_fetch(rangeindex, rangetable)->relid)
+extern Oid getrelid(Index rangeindex, List *rangetable);
/*
* Given an RTE and an attribute number, return the appropriate
diff --git a/src/include/rewrite/rewriteHandler.h b/src/include/rewrite/rewriteHandler.h
index 1831de4..ef54c57 100644
--- a/src/include/rewrite/rewriteHandler.h
+++ b/src/include/rewrite/rewriteHandler.h
@@ -18,6 +18,7 @@
#include "nodes/parsenodes.h"
extern List *QueryRewrite(Query *parsetree);
+extern void QueryRewriteExpr(Node *node, List *activeRIRs);
extern void AcquireRewriteLocks(Query *parsetree, bool forUpdatePushedDown);
extern Node *build_column_default(Relation rel, int attrno);
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index 72f8491..35819fa 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -128,6 +128,8 @@ typedef struct CachedPlan
bool is_oneshot; /* is it a "oneshot" plan? */
bool is_saved; /* is CachedPlan in a long-lived context? */
bool is_valid; /* is the stmt_list currently valid? */
+ Oid planUserId; /* is user-id that is assumed on this cached
+ plan, or InvalidOid if portable for anybody */
TransactionId saved_xmin; /* if valid, replan when TransactionXmin
* changes from this value */
int generation; /* parent's generation number for this plan */
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 58cc3f7..5e8441c 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -18,6 +18,7 @@
#include "catalog/pg_am.h"
#include "catalog/pg_class.h"
#include "catalog/pg_index.h"
+#include "catalog/pg_rowsecurity.h"
#include "fmgr.h"
#include "nodes/bitmapset.h"
#include "rewrite/prs2lock.h"
@@ -109,6 +110,7 @@ typedef struct RelationData
RuleLock *rd_rules; /* rewrite rules */
MemoryContext rd_rulescxt; /* private memory cxt for rd_rules, if any */
TriggerDesc *trigdesc; /* Trigger info, or NULL if rel has none */
+ RowSecurityDesc *rsdesc; /* Row-security policy, or NULL */
/*
* rd_options is set whenever rd_rel is loaded into the relcache entry.
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
new file mode 100644
index 0000000..04591df
--- /dev/null
+++ b/src/test/regress/expected/rowsecurity.out
@@ -0,0 +1,950 @@
+--
+-- Test of Row-level security feature
+--
+-- Clean up in case a prior regression run failed
+-- Suppress NOTICE messages when users/groups don't exist
+SET client_min_messages TO 'warning';
+DROP USER IF EXISTS rls_regress_user0;
+DROP USER IF EXISTS rls_regress_user1;
+DROP USER IF EXISTS rls_regress_user2;
+DROP SCHEMA IF EXISTS rls_regress_schema CASCADE;
+RESET client_min_messages;
+-- initial setup
+CREATE USER rls_regress_user0;
+CREATE USER rls_regress_user1;
+CREATE USER rls_regress_user2;
+CREATE SCHEMA rls_regress_schema;
+GRANT ALL ON SCHEMA rls_regress_schema TO public;
+SET search_path = rls_regress_schema;
+-- setup of malicious function
+CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool
+ COST 0.0000001 LANGUAGE plpgsql
+ AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END';
+GRANT EXECUTE ON FUNCTION f_leak(text) TO public;
+-- BASIC Row-Level Security Scenario
+SET SESSION AUTHORIZATION rls_regress_user0;
+CREATE TABLE uaccount (
+ pguser name primary key,
+ seclv int
+);
+INSERT INTO uaccount VALUES
+ ('rls_regress_user0', 99),
+ ('rls_regress_user1', 1),
+ ('rls_regress_user2', 2),
+ ('rls_regress_user3', 3);
+GRANT SELECT ON uaccount TO public;
+CREATE TABLE category (
+ cid int primary key,
+ cname text
+);
+GRANT ALL ON category TO public;
+INSERT INTO category VALUES
+ (11, 'novel'),
+ (22, 'science fiction'),
+ (33, 'technology'),
+ (44, 'manga');
+CREATE TABLE document (
+ did int primary key,
+ cid int references category(cid),
+ dlevel int not null,
+ dauthor name,
+ dtitle text
+);
+GRANT ALL ON document TO public;
+INSERT INTO document VALUES
+ ( 1, 11, 1, 'rls_regress_user1', 'my first novel'),
+ ( 2, 11, 2, 'rls_regress_user1', 'my second novel'),
+ ( 3, 22, 2, 'rls_regress_user1', 'my science fiction'),
+ ( 4, 44, 1, 'rls_regress_user1', 'my first manga'),
+ ( 5, 44, 2, 'rls_regress_user1', 'my second manga'),
+ ( 6, 22, 1, 'rls_regress_user2', 'great science fiction'),
+ ( 7, 33, 2, 'rls_regress_user2', 'great technology book'),
+ ( 8, 44, 1, 'rls_regress_user2', 'great manga');
+-- user's security level must higher than or equal to document's one
+ALTER TABLE document SET ROW SECURITY FOR ALL
+ TO (dlevel <= (SELECT seclv FROM uaccount WHERE pguser = current_user));
+-- viewpoint from rls_regress_user1
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE: f_leak => my first novel
+NOTICE: f_leak => my first manga
+NOTICE: f_leak => great science fiction
+NOTICE: f_leak => great manga
+ did | cid | dlevel | dauthor | dtitle
+-----+-----+--------+-------------------+-----------------------
+ 1 | 11 | 1 | rls_regress_user1 | my first novel
+ 4 | 44 | 1 | rls_regress_user1 | my first manga
+ 6 | 22 | 1 | rls_regress_user2 | great science fiction
+ 8 | 44 | 1 | rls_regress_user2 | great manga
+(4 rows)
+
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+NOTICE: f_leak => my first novel
+NOTICE: f_leak => my first manga
+NOTICE: f_leak => great science fiction
+NOTICE: f_leak => great manga
+ cid | did | dlevel | dauthor | dtitle | cname
+-----+-----+--------+-------------------+-----------------------+-----------------
+ 11 | 1 | 1 | rls_regress_user1 | my first novel | novel
+ 22 | 6 | 1 | rls_regress_user2 | great science fiction | science fiction
+ 44 | 8 | 1 | rls_regress_user2 | great manga | manga
+ 44 | 4 | 1 | rls_regress_user1 | my first manga | manga
+(4 rows)
+
+-- viewpoint from rls_regress_user2
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE: f_leak => my first novel
+NOTICE: f_leak => my second novel
+NOTICE: f_leak => my science fiction
+NOTICE: f_leak => my first manga
+NOTICE: f_leak => my second manga
+NOTICE: f_leak => great science fiction
+NOTICE: f_leak => great technology book
+NOTICE: f_leak => great manga
+ did | cid | dlevel | dauthor | dtitle
+-----+-----+--------+-------------------+-----------------------
+ 1 | 11 | 1 | rls_regress_user1 | my first novel
+ 2 | 11 | 2 | rls_regress_user1 | my second novel
+ 3 | 22 | 2 | rls_regress_user1 | my science fiction
+ 4 | 44 | 1 | rls_regress_user1 | my first manga
+ 5 | 44 | 2 | rls_regress_user1 | my second manga
+ 6 | 22 | 1 | rls_regress_user2 | great science fiction
+ 7 | 33 | 2 | rls_regress_user2 | great technology book
+ 8 | 44 | 1 | rls_regress_user2 | great manga
+(8 rows)
+
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+NOTICE: f_leak => my first novel
+NOTICE: f_leak => my second novel
+NOTICE: f_leak => my science fiction
+NOTICE: f_leak => my first manga
+NOTICE: f_leak => my second manga
+NOTICE: f_leak => great science fiction
+NOTICE: f_leak => great technology book
+NOTICE: f_leak => great manga
+ cid | did | dlevel | dauthor | dtitle | cname
+-----+-----+--------+-------------------+-----------------------+-----------------
+ 11 | 2 | 2 | rls_regress_user1 | my second novel | novel
+ 11 | 1 | 1 | rls_regress_user1 | my first novel | novel
+ 22 | 6 | 1 | rls_regress_user2 | great science fiction | science fiction
+ 22 | 3 | 2 | rls_regress_user1 | my science fiction | science fiction
+ 33 | 7 | 2 | rls_regress_user2 | great technology book | technology
+ 44 | 8 | 1 | rls_regress_user2 | great manga | manga
+ 44 | 5 | 2 | rls_regress_user1 | my second manga | manga
+ 44 | 4 | 1 | rls_regress_user1 | my first manga | manga
+(8 rows)
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+ QUERY PLAN
+----------------------------------------------------------
+ Subquery Scan on document
+ Filter: f_leak(document.dtitle)
+ -> Seq Scan on document document_1
+ Filter: (dlevel <= $0)
+ InitPlan 1 (returns $0)
+ -> Index Scan using uaccount_pkey on uaccount
+ Index Cond: (pguser = "current_user"())
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+ QUERY PLAN
+----------------------------------------------------------------------
+ Hash Join
+ Hash Cond: (category.cid = document.cid)
+ -> Seq Scan on category
+ -> Hash
+ -> Subquery Scan on document
+ Filter: f_leak(document.dtitle)
+ -> Seq Scan on document document_1
+ Filter: (dlevel <= $0)
+ InitPlan 1 (returns $0)
+ -> Index Scan using uaccount_pkey on uaccount
+ Index Cond: (pguser = "current_user"())
+(11 rows)
+
+-- only owner can change row-level security
+ALTER TABLE document SET ROW SECURITY FOR ALL TO (true); -- fail
+ERROR: must be owner of relation document
+ALTER TABLE document RESET ROW SECURITY FOR ALL; -- fail
+ERROR: must be owner of relation document
+SET SESSION AUTHORIZATION rls_regress_user0;
+ALTER TABLE document SET ROW SECURITY FOR ALL TO (dauthor = current_user);
+-- viewpoint from rls_regress_user1 again
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE: f_leak => my first novel
+NOTICE: f_leak => my second novel
+NOTICE: f_leak => my science fiction
+NOTICE: f_leak => my first manga
+NOTICE: f_leak => my second manga
+ did | cid | dlevel | dauthor | dtitle
+-----+-----+--------+-------------------+--------------------
+ 1 | 11 | 1 | rls_regress_user1 | my first novel
+ 2 | 11 | 2 | rls_regress_user1 | my second novel
+ 3 | 22 | 2 | rls_regress_user1 | my science fiction
+ 4 | 44 | 1 | rls_regress_user1 | my first manga
+ 5 | 44 | 2 | rls_regress_user1 | my second manga
+(5 rows)
+
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+NOTICE: f_leak => my first novel
+NOTICE: f_leak => my second novel
+NOTICE: f_leak => my science fiction
+NOTICE: f_leak => my first manga
+NOTICE: f_leak => my second manga
+ cid | did | dlevel | dauthor | dtitle | cname
+-----+-----+--------+-------------------+--------------------+-----------------
+ 11 | 1 | 1 | rls_regress_user1 | my first novel | novel
+ 11 | 2 | 2 | rls_regress_user1 | my second novel | novel
+ 22 | 3 | 2 | rls_regress_user1 | my science fiction | science fiction
+ 44 | 4 | 1 | rls_regress_user1 | my first manga | manga
+ 44 | 5 | 2 | rls_regress_user1 | my second manga | manga
+(5 rows)
+
+-- viewpoint from rls_regress_user2 again
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE: f_leak => great science fiction
+NOTICE: f_leak => great technology book
+NOTICE: f_leak => great manga
+ did | cid | dlevel | dauthor | dtitle
+-----+-----+--------+-------------------+-----------------------
+ 6 | 22 | 1 | rls_regress_user2 | great science fiction
+ 7 | 33 | 2 | rls_regress_user2 | great technology book
+ 8 | 44 | 1 | rls_regress_user2 | great manga
+(3 rows)
+
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+NOTICE: f_leak => great science fiction
+NOTICE: f_leak => great technology book
+NOTICE: f_leak => great manga
+ cid | did | dlevel | dauthor | dtitle | cname
+-----+-----+--------+-------------------+-----------------------+-----------------
+ 22 | 6 | 1 | rls_regress_user2 | great science fiction | science fiction
+ 33 | 7 | 2 | rls_regress_user2 | great technology book | technology
+ 44 | 8 | 1 | rls_regress_user2 | great manga | manga
+(3 rows)
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+ QUERY PLAN
+----------------------------------------------
+ Subquery Scan on document
+ Filter: f_leak(document.dtitle)
+ -> Seq Scan on document document_1
+ Filter: (dauthor = "current_user"())
+(4 rows)
+
+EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+ QUERY PLAN
+----------------------------------------------------
+ Nested Loop
+ -> Subquery Scan on document
+ Filter: f_leak(document.dtitle)
+ -> Seq Scan on document document_1
+ Filter: (dauthor = "current_user"())
+ -> Index Scan using category_pkey on category
+ Index Cond: (cid = document.cid)
+(7 rows)
+
+-- interaction of FK/PK constraints
+SET SESSION AUTHORIZATION rls_regress_user0;
+ALTER TABLE category SET ROW SECURITY FOR ALL
+ TO (CASE WHEN current_user = 'rls_regress_user1' THEN cid IN (11, 33)
+ WHEN current_user = 'rls_regress_user2' THEN cid IN (22, 44)
+ ELSE false END);
+-- cannot delete PK referenced by invisible FK
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document d full outer join category c on d.cid = c.cid;
+ did | cid | dlevel | dauthor | dtitle | cid | cname
+-----+-----+--------+-------------------+--------------------+-----+------------
+ 2 | 11 | 2 | rls_regress_user1 | my second novel | 11 | novel
+ 1 | 11 | 1 | rls_regress_user1 | my first novel | 11 | novel
+ | | | | | 33 | technology
+ 5 | 44 | 2 | rls_regress_user1 | my second manga | |
+ 4 | 44 | 1 | rls_regress_user1 | my first manga | |
+ 3 | 22 | 2 | rls_regress_user1 | my science fiction | |
+(6 rows)
+
+DELETE FROM category WHERE cid = 33; -- failed
+ERROR: update or delete on table "category" violates foreign key constraint "document_cid_fkey" on table "document"
+DETAIL: Key (cid)=(33) is still referenced from table "document".
+-- cannot insert FK referencing invisible PK
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document d full outer join category c on d.cid = c.cid;
+ did | cid | dlevel | dauthor | dtitle | cid | cname
+-----+-----+--------+-------------------+-----------------------+-----+-----------------
+ 6 | 22 | 1 | rls_regress_user2 | great science fiction | 22 | science fiction
+ 8 | 44 | 1 | rls_regress_user2 | great manga | 44 | manga
+ 7 | 33 | 2 | rls_regress_user2 | great technology book | |
+(3 rows)
+
+INSERT INTO document VALUES (10, 33, 1, current_user, 'hoge'); -- failed
+ERROR: insert or update on table "document" violates foreign key constraint "document_cid_fkey"
+DETAIL: Key (cid)=(33) is not present in table "category".
+-- database superuser can bypass RLS policy
+RESET SESSION AUTHORIZATION;
+SELECT * FROM document;
+ did | cid | dlevel | dauthor | dtitle
+-----+-----+--------+-------------------+-----------------------
+ 1 | 11 | 1 | rls_regress_user1 | my first novel
+ 2 | 11 | 2 | rls_regress_user1 | my second novel
+ 3 | 22 | 2 | rls_regress_user1 | my science fiction
+ 4 | 44 | 1 | rls_regress_user1 | my first manga
+ 5 | 44 | 2 | rls_regress_user1 | my second manga
+ 6 | 22 | 1 | rls_regress_user2 | great science fiction
+ 7 | 33 | 2 | rls_regress_user2 | great technology book
+ 8 | 44 | 1 | rls_regress_user2 | great manga
+(8 rows)
+
+SELECT * FROM category;
+ cid | cname
+-----+-----------------
+ 11 | novel
+ 22 | science fiction
+ 33 | technology
+ 44 | manga
+(4 rows)
+
+--
+-- Table inheritance and RLS policy
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+CREATE TABLE t1 (a int, junk1 text, b text) WITH OIDS;
+ALTER TABLE t1 DROP COLUMN junk1; -- just a disturbing factor
+GRANT ALL ON t1 TO public;
+COPY t1 FROM stdin WITH (oids);
+CREATE TABLE t2 (c float) INHERITS (t1);
+COPY t2 FROM stdin WITH (oids);
+CREATE TABLE t3 (c text, b text, a int) WITH OIDS;
+ALTER TABLE t3 INHERIT t1;
+COPY t3(a,b,c) FROM stdin WITH (oids);
+ALTER TABLE t1 SET ROW SECURITY FOR ALL TO (a % 2 = 0); -- be even number
+ALTER TABLE t2 SET ROW SECURITY FOR ALL TO (a % 2 = 1); -- be odd number
+SELECT * FROM t1;
+ a | b
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1;
+ QUERY PLAN
+-------------------------------------
+ Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+(8 rows)
+
+SELECT * FROM t1 WHERE f_leak(b);
+NOTICE: f_leak => bbb
+NOTICE: f_leak => ddd
+NOTICE: f_leak => abc
+NOTICE: f_leak => cde
+NOTICE: f_leak => xxx
+NOTICE: f_leak => yyy
+NOTICE: f_leak => zzz
+ a | b
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+ QUERY PLAN
+-------------------------------------
+ Append
+ -> Subquery Scan on t1
+ Filter: f_leak(t1.b)
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ Filter: f_leak(t2.b)
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+ Filter: f_leak(b)
+(11 rows)
+
+-- reference to system column
+SELECT oid, * FROM t1;
+ oid | a | b
+-----+---+-----
+ 102 | 2 | bbb
+ 104 | 4 | ddd
+ 201 | 1 | abc
+ 203 | 3 | cde
+ 301 | 1 | xxx
+ 302 | 2 | yyy
+ 303 | 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1;
+ QUERY PLAN
+-------------------------------------
+ Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+(8 rows)
+
+-- reference to whole-row reference
+SELECT *,t1 FROM t1;
+ a | b | t1
+---+-----+---------
+ 2 | bbb | (2,bbb)
+ 4 | ddd | (4,ddd)
+ 1 | abc | (1,abc)
+ 3 | cde | (3,cde)
+ 1 | xxx | (1,xxx)
+ 2 | yyy | (2,yyy)
+ 3 | zzz | (3,zzz)
+(7 rows)
+
+EXPLAIN (costs off) SELECT *,t1 FROM t1;
+ QUERY PLAN
+-------------------------------------
+ Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+(8 rows)
+
+-- for share/update lock
+SELECT * FROM t1 FOR SHARE;
+ a | b
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 FOR SHARE;
+ QUERY PLAN
+-------------------------------------------
+ LockRows
+ -> Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+(9 rows)
+
+SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+NOTICE: f_leak => bbb
+NOTICE: f_leak => ddd
+NOTICE: f_leak => abc
+NOTICE: f_leak => cde
+NOTICE: f_leak => xxx
+NOTICE: f_leak => yyy
+NOTICE: f_leak => zzz
+ a | b
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+ QUERY PLAN
+-------------------------------------------
+ LockRows
+ -> Append
+ -> Subquery Scan on t1
+ Filter: f_leak(t1.b)
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ Filter: f_leak(t2.b)
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+ Filter: f_leak(b)
+(12 rows)
+
+--
+-- COPY TO statement
+--
+COPY t1 TO stdout;
+2 bbb
+4 ddd
+COPY t1 TO stdout WITH OIDS;
+102 2 bbb
+104 4 ddd
+COPY t2(c,b) TO stdout WITH OIDS;
+201 1.1 abc
+203 3.3 cde
+COPY (SELECT * FROM t1) TO stdout;
+2 bbb
+4 ddd
+1 abc
+3 cde
+1 xxx
+2 yyy
+3 zzz
+COPY document TO stdout WITH OIDS; -- failed (no oid column)
+ERROR: table "document" does not have OIDs
+--
+-- recursive RLS and VIEWs in policy
+--
+CREATE TABLE s1 (a int, b text);
+INSERT INTO s1 (SELECT x, md5(x::text) FROM generate_series(-10,10) x);
+CREATE TABLE s2 (x int, y text);
+INSERT INTO s2 (SELECT x, md5(x::text) FROM generate_series(-6,6) x);
+CREATE VIEW v2 AS SELECT * FROM s2 WHERE y like '%af%';
+ALTER TABLE s1 SET ROW SECURITY FOR ALL
+ TO (a in (select x from s2 where y like '%2f%'));
+ALTER TABLE s2 SET ROW SECURITY FOR ALL
+ TO (x in (select a from s1 where b like '%22%'));
+SELECT * FROM s1 WHERE f_leak(b); -- fail (infinite recursion)
+ERROR: infinite recursion detected for relation "s1"
+ALTER TABLE s2 SET ROW SECURITY FOR ALL TO (x % 2 = 0);
+SELECT * FROM s1 WHERE f_leak(b); -- OK
+NOTICE: f_leak => c81e728d9d4c2f636f067f89cc14862c
+NOTICE: f_leak => a87ff679a2f3e71d9181a67b7542122c
+ a | b
+---+----------------------------------
+ 2 | c81e728d9d4c2f636f067f89cc14862c
+ 4 | a87ff679a2f3e71d9181a67b7542122c
+(2 rows)
+
+EXPLAIN SELECT * FROM only s1 WHERE f_leak(b);
+ QUERY PLAN
+---------------------------------------------------------------------------------------
+ Subquery Scan on s1 (cost=28.55..61.67 rows=205 width=36)
+ Filter: f_leak(s1.b)
+ -> Hash Join (cost=28.55..55.52 rows=615 width=36)
+ Hash Cond: (s1_1.a = s2.x)
+ -> Seq Scan on s1 s1_1 (cost=0.00..22.30 rows=1230 width=36)
+ -> Hash (cost=28.54..28.54 rows=1 width=4)
+ -> HashAggregate (cost=28.53..28.54 rows=1 width=4)
+ -> Subquery Scan on s2 (cost=0.00..28.52 rows=1 width=4)
+ Filter: (s2.y ~~ '%2f%'::text)
+ -> Seq Scan on s2 s2_1 (cost=0.00..28.45 rows=6 width=36)
+ Filter: ((x % 2) = 0)
+(11 rows)
+
+ALTER TABLE s1 SET ROW SECURITY FOR ALL
+ TO (a in (select x from v2)); -- using VIEW in RLS policy
+SELECT * FROM s1 WHERE f_leak(b); -- OK
+NOTICE: f_leak => 0267aaf632e87a63288a08331f22c7c3
+NOTICE: f_leak => 1679091c5a880faf6fb5e6087eb1b2dc
+ a | b
+----+----------------------------------
+ -4 | 0267aaf632e87a63288a08331f22c7c3
+ 6 | 1679091c5a880faf6fb5e6087eb1b2dc
+(2 rows)
+
+EXPLAIN (COSTS OFF) SELECT * FROM s1 WHERE f_leak(b);
+ QUERY PLAN
+----------------------------------------------------------
+ Subquery Scan on s1
+ Filter: f_leak(s1.b)
+ -> Hash Join
+ Hash Cond: (s1_1.a = s2.x)
+ -> Seq Scan on s1 s1_1
+ -> Hash
+ -> HashAggregate
+ -> Subquery Scan on s2
+ Filter: (s2.y ~~ '%af%'::text)
+ -> Seq Scan on s2 s2_1
+ Filter: ((x % 2) = 0)
+(11 rows)
+
+SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+ xx | x | y
+----+----+----------------------------------
+ -6 | -6 | 596a3d04481816330f07e4f97510c28f
+ -4 | -4 | 0267aaf632e87a63288a08331f22c7c3
+ 2 | 2 | c81e728d9d4c2f636f067f89cc14862c
+(3 rows)
+
+EXPLAIN (COSTS OFF) SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+ QUERY PLAN
+--------------------------------------------------------------------
+ Subquery Scan on s2
+ Filter: (s2.y ~~ '%28%'::text)
+ -> Seq Scan on s2 s2_1
+ Filter: ((x % 2) = 0)
+ SubPlan 1
+ -> Limit
+ -> Subquery Scan on s1
+ -> Nested Loop Semi Join
+ Join Filter: (s1_1.a = s2_2.x)
+ -> Seq Scan on s1 s1_1
+ -> Materialize
+ -> Subquery Scan on s2_2
+ Filter: (s2_2.y ~~ '%af%'::text)
+ -> Seq Scan on s2 s2_3
+ Filter: ((x % 2) = 0)
+(15 rows)
+
+ALTER TABLE s2 SET ROW SECURITY FOR ALL
+ TO (x in (select a from s1 where b like '%d2%'));
+SELECT * FROM s1 WHERE f_leak(b); -- fail (infinite recursion via view)
+ERROR: infinite recursion detected for relation "s1"
+-- prepared statement with rls_regress_user0 privilege
+PREPARE p1(int) AS SELECT * FROM t1 WHERE a <= $1;
+EXECUTE p1(2);
+ a | b
+---+-----
+ 2 | bbb
+ 1 | abc
+ 1 | xxx
+ 2 | yyy
+(4 rows)
+
+EXPLAIN (costs off) EXECUTE p1(2);
+ QUERY PLAN
+----------------------------------------------------
+ Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a <= 2) AND ((a % 2) = 0))
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a <= 2) AND ((a % 2) = 1))
+ -> Seq Scan on t3
+ Filter: (a <= 2)
+(9 rows)
+
+-- superuser is allowed to bypass RLS checks
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1 WHERE f_leak(b);
+NOTICE: f_leak => aaa
+NOTICE: f_leak => bbb
+NOTICE: f_leak => ccc
+NOTICE: f_leak => ddd
+NOTICE: f_leak => abc
+NOTICE: f_leak => bcd
+NOTICE: f_leak => cde
+NOTICE: f_leak => def
+NOTICE: f_leak => xxx
+NOTICE: f_leak => yyy
+NOTICE: f_leak => zzz
+ a | b
+---+-----
+ 1 | aaa
+ 2 | bbb
+ 3 | ccc
+ 4 | ddd
+ 1 | abc
+ 2 | bcd
+ 3 | cde
+ 4 | def
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(11 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+ QUERY PLAN
+---------------------------
+ Append
+ -> Seq Scan on t1
+ Filter: f_leak(b)
+ -> Seq Scan on t2
+ Filter: f_leak(b)
+ -> Seq Scan on t3
+ Filter: f_leak(b)
+(7 rows)
+
+-- plan cache should be invalidated
+EXECUTE p1(2);
+ a | b
+---+-----
+ 1 | aaa
+ 2 | bbb
+ 1 | abc
+ 2 | bcd
+ 1 | xxx
+ 2 | yyy
+(6 rows)
+
+EXPLAIN (costs off) EXECUTE p1(2);
+ QUERY PLAN
+--------------------------
+ Append
+ -> Seq Scan on t1
+ Filter: (a <= 2)
+ -> Seq Scan on t2
+ Filter: (a <= 2)
+ -> Seq Scan on t3
+ Filter: (a <= 2)
+(7 rows)
+
+PREPARE p2(int) AS SELECT * FROM t1 WHERE a = $1;
+EXECUTE p2(2);
+ a | b
+---+-----
+ 2 | bbb
+ 2 | bcd
+ 2 | yyy
+(3 rows)
+
+EXPLAIN (costs off) EXECUTE p2(2);
+ QUERY PLAN
+-------------------------
+ Append
+ -> Seq Scan on t1
+ Filter: (a = 2)
+ -> Seq Scan on t2
+ Filter: (a = 2)
+ -> Seq Scan on t3
+ Filter: (a = 2)
+(7 rows)
+
+-- also, case when privilege switch from superuser
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXECUTE p2(2);
+ a | b
+---+-----
+ 2 | bbb
+ 2 | yyy
+(2 rows)
+
+EXPLAIN (costs off) EXECUTE p2(2);
+ QUERY PLAN
+---------------------------------------------------
+ Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a = 2) AND ((a % 2) = 0))
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a = 2) AND ((a % 2) = 1))
+ -> Seq Scan on t3
+ Filter: (a = 2)
+(9 rows)
+
+--
+-- UPDATE / DELETE and Row-level security
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXPLAIN (costs off) UPDATE t1 SET b = b || b WHERE f_leak(b);
+ QUERY PLAN
+-------------------------------------
+ Update on t1
+ -> Subquery Scan on t1_1
+ Filter: f_leak(t1_1.b)
+ -> Seq Scan on t1 t1_2
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ Filter: f_leak(t2.b)
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+ Filter: f_leak(b)
+(11 rows)
+
+UPDATE t1 SET b = b || b WHERE f_leak(b);
+NOTICE: f_leak => bbb
+NOTICE: f_leak => ddd
+NOTICE: f_leak => abc
+NOTICE: f_leak => cde
+NOTICE: f_leak => xxx
+NOTICE: f_leak => yyy
+NOTICE: f_leak => zzz
+EXPLAIN (costs off) UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b);
+ QUERY PLAN
+-------------------------------------
+ Update on t1
+ -> Subquery Scan on t1_1
+ Filter: f_leak(t1_1.b)
+ -> Seq Scan on t1 t1_2
+ Filter: ((a % 2) = 0)
+(5 rows)
+
+UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b);
+NOTICE: f_leak => bbbbbb
+NOTICE: f_leak => dddddd
+-- returning clause with system column
+UPDATE only t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+NOTICE: f_leak => bbbbbb_updt
+NOTICE: f_leak => dddddd_updt
+ oid | a | b | t1
+-----+---+-------------+-----------------
+ 102 | 2 | bbbbbb_updt | (2,bbbbbb_updt)
+ 104 | 4 | dddddd_updt | (4,dddddd_updt)
+(2 rows)
+
+UPDATE t1 SET b = b WHERE f_leak(b) RETURNING *;
+NOTICE: f_leak => bbbbbb_updt
+NOTICE: f_leak => dddddd_updt
+NOTICE: f_leak => abcabc
+NOTICE: f_leak => cdecde
+NOTICE: f_leak => xxxxxx
+NOTICE: f_leak => yyyyyy
+NOTICE: f_leak => zzzzzz
+ a | b
+---+-------------
+ 2 | bbbbbb_updt
+ 4 | dddddd_updt
+ 1 | abcabc
+ 3 | cdecde
+ 1 | xxxxxx
+ 2 | yyyyyy
+ 3 | zzzzzz
+(7 rows)
+
+UPDATE t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+NOTICE: f_leak => bbbbbb_updt
+NOTICE: f_leak => dddddd_updt
+NOTICE: f_leak => abcabc
+NOTICE: f_leak => cdecde
+NOTICE: f_leak => xxxxxx
+NOTICE: f_leak => yyyyyy
+NOTICE: f_leak => zzzzzz
+ oid | a | b | t1
+-----+---+-------------+-----------------
+ 102 | 2 | bbbbbb_updt | (2,bbbbbb_updt)
+ 104 | 4 | dddddd_updt | (4,dddddd_updt)
+ 201 | 1 | abcabc | (1,abcabc)
+ 203 | 3 | cdecde | (3,cdecde)
+ 301 | 1 | xxxxxx | (1,xxxxxx)
+ 302 | 2 | yyyyyy | (2,yyyyyy)
+ 303 | 3 | zzzzzz | (3,zzzzzz)
+(7 rows)
+
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1;
+ a | b
+---+-------------
+ 1 | aaa
+ 3 | ccc
+ 2 | bbbbbb_updt
+ 4 | dddddd_updt
+ 2 | bcd
+ 4 | def
+ 1 | abcabc
+ 3 | cdecde
+ 1 | xxxxxx
+ 2 | yyyyyy
+ 3 | zzzzzz
+(11 rows)
+
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXPLAIN (costs off) DELETE FROM only t1 WHERE f_leak(b);
+ QUERY PLAN
+-------------------------------------
+ Delete on t1
+ -> Subquery Scan on t1_1
+ Filter: f_leak(t1_1.b)
+ -> Seq Scan on t1 t1_2
+ Filter: ((a % 2) = 0)
+(5 rows)
+
+EXPLAIN (costs off) DELETE FROM t1 WHERE f_leak(b);
+ QUERY PLAN
+-------------------------------------
+ Delete on t1
+ -> Subquery Scan on t1_1
+ Filter: f_leak(t1_1.b)
+ -> Seq Scan on t1 t1_2
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ Filter: f_leak(t2.b)
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+ Filter: f_leak(b)
+(11 rows)
+
+DELETE FROM only t1 WHERE f_leak(b) RETURNING oid, *, t1;
+NOTICE: f_leak => bbbbbb_updt
+NOTICE: f_leak => dddddd_updt
+ oid | a | b | t1
+-----+---+-------------+-----------------
+ 102 | 2 | bbbbbb_updt | (2,bbbbbb_updt)
+ 104 | 4 | dddddd_updt | (4,dddddd_updt)
+(2 rows)
+
+DELETE FROM t1 WHERE f_leak(b) RETURNING oid, *, t1;
+NOTICE: f_leak => abcabc
+NOTICE: f_leak => cdecde
+NOTICE: f_leak => xxxxxx
+NOTICE: f_leak => yyyyyy
+NOTICE: f_leak => zzzzzz
+ oid | a | b | t1
+-----+---+--------+------------
+ 201 | 1 | abcabc | (1,abcabc)
+ 203 | 3 | cdecde | (3,cdecde)
+ 301 | 1 | xxxxxx | (1,xxxxxx)
+ 302 | 2 | yyyyyy | (2,yyyyyy)
+ 303 | 3 | zzzzzz | (3,zzzzzz)
+(5 rows)
+
+--
+-- Test psql \dt+ command
+--
+ALTER TABLE category RESET ROW SECURITY FOR ALL; -- too long qual
+\dt+
+ List of relations
+ Schema | Name | Type | Owner | Size | Description | Row-security
+--------------------+----------+-------+-------------------+------------+-------------+----------------------------------
+ rls_regress_schema | category | table | rls_regress_user0 | 16 kB | |
+ rls_regress_schema | document | table | rls_regress_user0 | 16 kB | | (dauthor = "current_user"())
+ rls_regress_schema | s1 | table | rls_regress_user0 | 16 kB | | (a IN ( SELECT v2.x +
+ | | | | | | FROM v2))
+ rls_regress_schema | s2 | table | rls_regress_user0 | 16 kB | | (x IN ( SELECT s1.a +
+ | | | | | | FROM s1 +
+ | | | | | | WHERE (s1.b ~~ '%d2%'::text)))
+ rls_regress_schema | t1 | table | rls_regress_user0 | 16 kB | | ((a % 2) = 0)
+ rls_regress_schema | t2 | table | rls_regress_user0 | 16 kB | | ((a % 2) = 1)
+ rls_regress_schema | t3 | table | rls_regress_user0 | 16 kB | |
+ rls_regress_schema | uaccount | table | rls_regress_user0 | 8192 bytes | |
+(8 rows)
+
+--
+-- Clean up objects
+--
+RESET SESSION AUTHORIZATION;
+DROP SCHEMA rls_regress_schema CASCADE;
+NOTICE: drop cascades to 10 other objects
+DETAIL: drop cascades to function f_leak(text)
+drop cascades to table uaccount
+drop cascades to table category
+drop cascades to table document
+drop cascades to table t1
+drop cascades to table t2
+drop cascades to table t3
+drop cascades to table s1
+drop cascades to table s2
+drop cascades to view v2
+DROP USER rls_regress_user0;
+DROP USER rls_regress_user1;
+DROP USER rls_regress_user2;
diff --git a/src/test/regress/expected/sanity_check.out b/src/test/regress/expected/sanity_check.out
index 432d39a..3b9123d 100644
--- a/src/test/regress/expected/sanity_check.out
+++ b/src/test/regress/expected/sanity_check.out
@@ -120,6 +120,7 @@ SELECT relname, relhasindex
pg_proc | t
pg_range | t
pg_rewrite | t
+ pg_rowsecurity | t
pg_seclabel | t
pg_shdepend | t
pg_shdescription | t
@@ -166,7 +167,7 @@ SELECT relname, relhasindex
timetz_tbl | f
tinterval_tbl | f
varchar_tbl | f
-(155 rows)
+(156 rows)
--
-- another sanity check: every system catalog that has OIDs should have
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 3e6b306..f2d75f5 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 matview
+test: privileges rowsecurity security_label collate matview
# ----------
# Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 3ad289f..d44e89b 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -93,6 +93,7 @@ test: delete
test: namespace
test: prepared_xacts
test: privileges
+test: rowsecurity
test: security_label
test: collate
test: matview
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
new file mode 100644
index 0000000..55d4aad
--- /dev/null
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -0,0 +1,298 @@
+--
+-- Test of Row-level security feature
+--
+
+-- Clean up in case a prior regression run failed
+
+-- Suppress NOTICE messages when users/groups don't exist
+SET client_min_messages TO 'warning';
+
+DROP USER IF EXISTS rls_regress_user0;
+DROP USER IF EXISTS rls_regress_user1;
+DROP USER IF EXISTS rls_regress_user2;
+
+DROP SCHEMA IF EXISTS rls_regress_schema CASCADE;
+
+RESET client_min_messages;
+
+-- initial setup
+CREATE USER rls_regress_user0;
+CREATE USER rls_regress_user1;
+CREATE USER rls_regress_user2;
+
+CREATE SCHEMA rls_regress_schema;
+GRANT ALL ON SCHEMA rls_regress_schema TO public;
+SET search_path = rls_regress_schema;
+
+-- setup of malicious function
+CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool
+ COST 0.0000001 LANGUAGE plpgsql
+ AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END';
+GRANT EXECUTE ON FUNCTION f_leak(text) TO public;
+
+-- BASIC Row-Level Security Scenario
+
+SET SESSION AUTHORIZATION rls_regress_user0;
+CREATE TABLE uaccount (
+ pguser name primary key,
+ seclv int
+);
+INSERT INTO uaccount VALUES
+ ('rls_regress_user0', 99),
+ ('rls_regress_user1', 1),
+ ('rls_regress_user2', 2),
+ ('rls_regress_user3', 3);
+GRANT SELECT ON uaccount TO public;
+
+CREATE TABLE category (
+ cid int primary key,
+ cname text
+);
+GRANT ALL ON category TO public;
+INSERT INTO category VALUES
+ (11, 'novel'),
+ (22, 'science fiction'),
+ (33, 'technology'),
+ (44, 'manga');
+
+CREATE TABLE document (
+ did int primary key,
+ cid int references category(cid),
+ dlevel int not null,
+ dauthor name,
+ dtitle text
+);
+GRANT ALL ON document TO public;
+INSERT INTO document VALUES
+ ( 1, 11, 1, 'rls_regress_user1', 'my first novel'),
+ ( 2, 11, 2, 'rls_regress_user1', 'my second novel'),
+ ( 3, 22, 2, 'rls_regress_user1', 'my science fiction'),
+ ( 4, 44, 1, 'rls_regress_user1', 'my first manga'),
+ ( 5, 44, 2, 'rls_regress_user1', 'my second manga'),
+ ( 6, 22, 1, 'rls_regress_user2', 'great science fiction'),
+ ( 7, 33, 2, 'rls_regress_user2', 'great technology book'),
+ ( 8, 44, 1, 'rls_regress_user2', 'great manga');
+
+-- user's security level must higher than or equal to document's one
+ALTER TABLE document SET ROW SECURITY FOR ALL
+ TO (dlevel <= (SELECT seclv FROM uaccount WHERE pguser = current_user));
+
+-- viewpoint from rls_regress_user1
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+-- viewpoint from rls_regress_user2
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+-- only owner can change row-level security
+ALTER TABLE document SET ROW SECURITY FOR ALL TO (true); -- fail
+ALTER TABLE document RESET ROW SECURITY FOR ALL; -- fail
+
+SET SESSION AUTHORIZATION rls_regress_user0;
+ALTER TABLE document SET ROW SECURITY FOR ALL TO (dauthor = current_user);
+
+-- viewpoint from rls_regress_user1 again
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+-- viewpoint from rls_regress_user2 again
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+-- interaction of FK/PK constraints
+SET SESSION AUTHORIZATION rls_regress_user0;
+ALTER TABLE category SET ROW SECURITY FOR ALL
+ TO (CASE WHEN current_user = 'rls_regress_user1' THEN cid IN (11, 33)
+ WHEN current_user = 'rls_regress_user2' THEN cid IN (22, 44)
+ ELSE false END);
+
+-- cannot delete PK referenced by invisible FK
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document d full outer join category c on d.cid = c.cid;
+DELETE FROM category WHERE cid = 33; -- failed
+
+-- cannot insert FK referencing invisible PK
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document d full outer join category c on d.cid = c.cid;
+INSERT INTO document VALUES (10, 33, 1, current_user, 'hoge'); -- failed
+
+-- database superuser can bypass RLS policy
+RESET SESSION AUTHORIZATION;
+SELECT * FROM document;
+SELECT * FROM category;
+
+--
+-- Table inheritance and RLS policy
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+
+CREATE TABLE t1 (a int, junk1 text, b text) WITH OIDS;
+ALTER TABLE t1 DROP COLUMN junk1; -- just a disturbing factor
+GRANT ALL ON t1 TO public;
+
+COPY t1 FROM stdin WITH (oids);
+101 1 aaa
+102 2 bbb
+103 3 ccc
+104 4 ddd
+\.
+
+CREATE TABLE t2 (c float) INHERITS (t1);
+COPY t2 FROM stdin WITH (oids);
+201 1 abc 1.1
+202 2 bcd 2.2
+203 3 cde 3.3
+204 4 def 4.4
+\.
+
+CREATE TABLE t3 (c text, b text, a int) WITH OIDS;
+ALTER TABLE t3 INHERIT t1;
+COPY t3(a,b,c) FROM stdin WITH (oids);
+301 1 xxx X
+302 2 yyy Y
+303 3 zzz Z
+\.
+
+ALTER TABLE t1 SET ROW SECURITY FOR ALL TO (a % 2 = 0); -- be even number
+ALTER TABLE t2 SET ROW SECURITY FOR ALL TO (a % 2 = 1); -- be odd number
+
+SELECT * FROM t1;
+EXPLAIN (costs off) SELECT * FROM t1;
+
+SELECT * FROM t1 WHERE f_leak(b);
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+
+-- reference to system column
+SELECT oid, * FROM t1;
+EXPLAIN (costs off) SELECT * FROM t1;
+
+-- reference to whole-row reference
+SELECT *,t1 FROM t1;
+EXPLAIN (costs off) SELECT *,t1 FROM t1;
+
+-- for share/update lock
+SELECT * FROM t1 FOR SHARE;
+EXPLAIN (costs off) SELECT * FROM t1 FOR SHARE;
+
+SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+
+--
+-- COPY TO statement
+--
+COPY t1 TO stdout;
+COPY t1 TO stdout WITH OIDS;
+COPY t2(c,b) TO stdout WITH OIDS;
+COPY (SELECT * FROM t1) TO stdout;
+COPY document TO stdout WITH OIDS; -- failed (no oid column)
+
+--
+-- recursive RLS and VIEWs in policy
+--
+CREATE TABLE s1 (a int, b text);
+INSERT INTO s1 (SELECT x, md5(x::text) FROM generate_series(-10,10) x);
+
+CREATE TABLE s2 (x int, y text);
+INSERT INTO s2 (SELECT x, md5(x::text) FROM generate_series(-6,6) x);
+CREATE VIEW v2 AS SELECT * FROM s2 WHERE y like '%af%';
+
+ALTER TABLE s1 SET ROW SECURITY FOR ALL
+ TO (a in (select x from s2 where y like '%2f%'));
+
+ALTER TABLE s2 SET ROW SECURITY FOR ALL
+ TO (x in (select a from s1 where b like '%22%'));
+
+SELECT * FROM s1 WHERE f_leak(b); -- fail (infinite recursion)
+
+ALTER TABLE s2 SET ROW SECURITY FOR ALL TO (x % 2 = 0);
+
+SELECT * FROM s1 WHERE f_leak(b); -- OK
+EXPLAIN SELECT * FROM only s1 WHERE f_leak(b);
+
+ALTER TABLE s1 SET ROW SECURITY FOR ALL
+ TO (a in (select x from v2)); -- using VIEW in RLS policy
+SELECT * FROM s1 WHERE f_leak(b); -- OK
+EXPLAIN (COSTS OFF) SELECT * FROM s1 WHERE f_leak(b);
+
+SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+EXPLAIN (COSTS OFF) SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+
+ALTER TABLE s2 SET ROW SECURITY FOR ALL
+ TO (x in (select a from s1 where b like '%d2%'));
+SELECT * FROM s1 WHERE f_leak(b); -- fail (infinite recursion via view)
+
+-- prepared statement with rls_regress_user0 privilege
+PREPARE p1(int) AS SELECT * FROM t1 WHERE a <= $1;
+EXECUTE p1(2);
+EXPLAIN (costs off) EXECUTE p1(2);
+
+-- superuser is allowed to bypass RLS checks
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1 WHERE f_leak(b);
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+
+-- plan cache should be invalidated
+EXECUTE p1(2);
+EXPLAIN (costs off) EXECUTE p1(2);
+
+PREPARE p2(int) AS SELECT * FROM t1 WHERE a = $1;
+EXECUTE p2(2);
+EXPLAIN (costs off) EXECUTE p2(2);
+
+-- also, case when privilege switch from superuser
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXECUTE p2(2);
+EXPLAIN (costs off) EXECUTE p2(2);
+
+--
+-- UPDATE / DELETE and Row-level security
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXPLAIN (costs off) UPDATE t1 SET b = b || b WHERE f_leak(b);
+UPDATE t1 SET b = b || b WHERE f_leak(b);
+
+EXPLAIN (costs off) UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b);
+UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b);
+
+-- returning clause with system column
+UPDATE only t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+UPDATE t1 SET b = b WHERE f_leak(b) RETURNING *;
+UPDATE t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1;
+
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXPLAIN (costs off) DELETE FROM only t1 WHERE f_leak(b);
+EXPLAIN (costs off) DELETE FROM t1 WHERE f_leak(b);
+
+DELETE FROM only t1 WHERE f_leak(b) RETURNING oid, *, t1;
+DELETE FROM t1 WHERE f_leak(b) RETURNING oid, *, t1;
+
+--
+-- Test psql \dt+ command
+--
+ALTER TABLE category RESET ROW SECURITY FOR ALL; -- too long qual
+\dt+
+
+--
+-- Clean up objects
+--
+RESET SESSION AUTHORIZATION;
+
+DROP SCHEMA rls_regress_schema CASCADE;
+
+DROP USER rls_regress_user0;
+DROP USER rls_regress_user1;
+DROP USER rls_regress_user2;
With the current HEAD and v3 patch I'm seeing the following error with
"make check".
------
============== creating temporary installation ==============
============== initializing database system ==============
pg_regress: initdb failed
Examine /usr/local/src/postgres.patched.v3/src/test/regress/log/initdb.log
for the reason.
Command was:
"/usr/local/src/postgres.patched.v3/src/test/regress/./tmp_check/install//usr/local/pgsql/bin/initdb"
-D "/usr/local/src/postgres.patched.v3/src/test/regress/./tmp_check/data"
-L
"/usr/local/src/postgres.patched.v3/src/test/regress/./tmp_check/install//usr/local/pgsql/share"
--noclean --nosync >
"/usr/local/src/postgres.patched.v3/src/test/regress/log/initdb.log" 2>&1
make[1]: *** [check] Error 2
make[1]: Leaving directory
`/usr/local/src/postgres.patched.v3/src/test/regress'
make: *** [check] Error 2
__________________________________________________________________________________
*Mike Blackwell | Technical Analyst, Distribution Services/Rollout
Management | RR Donnelley*
1750 Wallace Ave | St Charles, IL 60174-3401
Office: 630.313.7818
Mike.Blackwell@rrd.com
http://www.rrdonnelley.com
<http://www.rrdonnelley.com/>
* <Mike.Blackwell@rrd.com>*
Import Notes
Reply to msg id not found: CADyhKSVoCHMnnGX0Z2b8dgfCUoE1XGdtbAz2HXE4+Q3ajgoxrQ@mail.gmail.com
Current head 4cbe3ac3e86790d05c569de4585e5075a62a9b41 -> patch applies
correct (only change needed in parallel_schedule).
However it fails on own regression tests (other tests pass).
Regards,
Karol
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 7/18/13 7:57 PM, Karol Trzcionka wrote:
Current head 4cbe3ac3e86790d05c569de4585e5075a62a9b41 -> patch applies
correct (only change needed in parallel_schedule).
However it fails on own regression tests (other tests pass).
I got a rejected hunk in src/backend/nodes/nodeFuncs.c as well as that
parallel_schedule issue. Maybe you didn't get the nodeFuncs change but
didn't notice that? That might explain why the tests didn't work for
you either.
Attached is an updated patch where I tried to only fix the two small
hunks of bit rot. I get "All 140 tests passed" here, on a Mac no less.
I did a brief code scan through the patch just to get a feel for how the
feature is put together, and what you'd need to know for a deeper
review. (I'm trying to get customer time approved to work on this a lot
more) The code was easier to follow than I expected. The way it
completely avoids even getting into the security label integration yet
seems like a successful design partitioning. This isn't nearly as scary
as the SEPostgres patches. There are some useful looking utility
functions that dump information about what's going on too.
The bulk of the complexity is how the feature modifies query nodes to
restrict what rows come through them. Some familiarity with that part
of the code is what you'd need to take on reviewing this in detail.
That and a week of time to spend trudging through it. If anyone is
looking for an educational challenge on query execution, marching
through all of these changes to validate they work as expected would do
that.
--
Greg Smith 2ndQuadrant US greg@2ndQuadrant.com Baltimore, MD
PostgreSQL Training, Services, and 24x7 Support www.2ndQuadrant.com
Attachments:
pgsql-v9.4-row-level-security.v3b.patchtext/plain; charset=UTF-8; name=pgsql-v9.4-row-level-security.v3b.patch; x-mac-creator=0; x-mac-type=0Download
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
new file mode 100644
index 6715782..6f3c8fc
*** a/doc/src/sgml/catalogs.sgml
--- b/doc/src/sgml/catalogs.sgml
***************
*** 234,239 ****
--- 234,244 ----
</row>
<row>
+ <entry><link linkend="catalog-pg-rowlevelsec"><structname>pg_rowlevelsec</structname></link></entry>
+ <entry>row-level security policy of relation</entry>
+ </row>
+
+ <row>
<entry><link linkend="catalog-pg-seclabel"><structname>pg_seclabel</structname></link></entry>
<entry>security labels on database objects</entry>
</row>
***************
*** 1848,1853 ****
--- 1853,1868 ----
</row>
<row>
+ <entry><structfield>relhasrowsecurity</structfield></entry>
+ <entry><type>bool</type></entry>
+ <entry></entry>
+ <entry>
+ True if table has row-security policy; see
+ <link linkend="catalog-pg-rowsecurity"><structname>pg_rowsecurity</structname></link> catalog
+ </entry>
+ </row>
+
+ <row>
<entry><structfield>relhassubclass</structfield></entry>
<entry><type>bool</type></entry>
<entry></entry>
***************
*** 5112,5117 ****
--- 5127,5182 ----
</sect1>
+ <sect1 id="catalog-pg-rowsecurity">
+ <title><structname>pg_rowlevelsec</structname></title>
+
+ <indexterm zone="catalog-pg-rowsecurity">
+ <primary>pg_rowsecurity</primary>
+ </indexterm>
+ <para>
+ The catalog <structname>pg_rowsecurity</structname> stores expression
+ tree of row-security policy to be performed on a particular relation.
+ </para>
+ <table>
+ <title><structname>pg_rowsecurity</structname> Columns</title>
+ <tgroup cols="4">
+ <thead>
+ <row>
+ <entry>Name</entry>
+ <entry>Type</entry>
+ <entry>References</entry>
+ <entry>Description</entry>
+ </row>
+ </thead>
+ <tbody>
+ <row>
+ <entry><structfield>rsecrelid</structfield></entry>
+ <entry><type>oid</type></entry>
+ <entry><literal><link linkend="catalog-pg-class"><structname>pg_class</structname></link>.oid</literal></entry>
+ <entry>The table this row-security is for</entry>
+ </row>
+ <row>
+ <entry><structfield>rseccmd</structfield></entry>
+ <entry><type>char</type></entry>
+ <entry></entry>
+ <entry>The command this row-security is for. 'a' meaning all is only possible value right now.</entry>
+ </row>
+ <row>
+ <entry><structfield>rsecqual</structfield></entry>
+ <entry><type>pg_node_tree</type></entry>
+ <entry></entry>
+ <entry>An expression tree to be performed as rowl-security policy</entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+ <note>
+ <para>
+ <literal>pg_class.relhasrowlevelsec</literal>
+ must be true if a table has row-level security policy in this catalog.
+ </para>
+ </note>
+ </sect1>
<sect1 id="catalog-pg-seclabel">
<title><structname>pg_seclabel</structname></title>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
new file mode 100644
index 2609d4a..b380852
*** a/doc/src/sgml/ref/alter_table.sgml
--- b/doc/src/sgml/ref/alter_table.sgml
*************** ALTER TABLE [ IF EXISTS ] <replaceable c
*** 69,80 ****
--- 69,84 ----
NOT OF
OWNER TO <replaceable class="PARAMETER">new_owner</replaceable>
SET TABLESPACE <replaceable class="PARAMETER">new_tablespace</replaceable>
+ SET ROW SECURITY FOR <replaceable class="PARAMETER">rowsec_command</replaceable> TO (<replaceable class="PARAMETER">condition</replaceable>)
+ RESET ROW SECURITY FOR <replaceable class="PARAMETER">rowsec_command</replaceable>
<phrase>and <replaceable class="PARAMETER">table_constraint_using_index</replaceable> is:</phrase>
[ CONSTRAINT <replaceable class="PARAMETER">constraint_name</replaceable> ]
{ UNIQUE | PRIMARY KEY } USING INDEX <replaceable class="PARAMETER">index_name</replaceable>
[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+ <phrase>and <replaceable class="PARAMETER">rowsec_command</replaceable> is:</phrase>
+ { ALL | SELECT | INSERT | UPDATE | DELETE }
</synopsis>
</refsynopsisdiv>
*************** ALTER TABLE [ IF EXISTS ] <replaceable c
*** 580,585 ****
--- 584,614 ----
</varlistentry>
<varlistentry>
+ <term><literal>SET ROW SECURITY FOR <replaceable class="PARAMETER">rowsec_command</replaceable> TO (<replaceable class="PARAMETER">condition</replaceable>)</literal></term>
+ <listitem>
+ <para>
+ This form set row-level security policy of the table.
+ Supplied <replaceable class="PARAMETER">condition</replaceable> performs
+ as if it is implicitly appended to the qualifiers of <literal>WHERE</literal>
+ clause, although mechanism guarantees to evaluate this condition earlier
+ than any other user given condition.
+ <literal>ALL</> is the only supported command type right now.
+ See also <xref linkend="ROW-SECURITY">.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>RESET ROW SECURITY FOR <replaceable class="PARAMETER">rowsec_command</replaceable></literal></term>
+ <listitem>
+ <para>
+ This form reset row-level security policy of the table, if exists.
+ <literal>ALL</> is the only supported command type right now.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
<term><literal>RENAME</literal></term>
<listitem>
<para>
*************** ALTER TABLE [ IF EXISTS ] <replaceable c
*** 821,826 ****
--- 850,869 ----
</listitem>
</varlistentry>
+ <varlistentry>
+ <term><replaceable class="PARAMETER">condition</replaceable></term>
+ <listitem>
+ <para>
+ An expression that returns a value of type boolean. Expect for a case
+ when queries are executed with superuser privilege, only rows for which
+ this expression returns true will be fetched, updated or deleted.
+ This expression can reference columns of the relation being configured.
+ Sub-queries can be contained within expression tree, unless referenced
+ relation recursively references the same relation.
+ </para>
+ </listitem>
+ </varlistentry>
+
</variablelist>
</refsect1>
diff --git a/doc/src/sgml/user-manag.sgml b/doc/src/sgml/user-manag.sgml
new file mode 100644
index 177ac7a..34a699e
*** a/doc/src/sgml/user-manag.sgml
--- b/doc/src/sgml/user-manag.sgml
*************** DROP ROLE <replaceable>name</replaceable
*** 439,442 ****
--- 439,587 ----
</para>
</sect1>
+ <sect1 id="row-security">
+ <title>Row-security</title>
+ <para>
+ <productname>PostgreSQL</productname> v9.3 or later provides
+ row-security feature, like several commercial database
+ management system. It allows table owner to assign a particular
+ condition that performs as a security policy of the table; only
+ rows that satisfies the condition should be visible, except for
+ a case when superuser runs queries.
+ </para>
+ <para>
+ Row-security policy can be set using <literal>SET ROW SECURITY</literal>
+ command of <xref linkend="SQL-ALTERTABLE"> 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.
+ </para>
+ <para>
+ For example, the following <literal>customer</literal> table has
+ <literal>uname</literal> field to store user name, and it assume
+ we don't want to expose any properties of other customers.
+ The following command set <literal>current_user = uname</literal>
+ as row-security policy on the <literal>customer</literal> table.
+ <screen>
+ postgres=> ALTER TABLE customer SET ROW SECURITY
+ FOR ALL TO (current_user = uname);
+ ALTER TABLE
+ </screen>
+ <xref linkend="SQL-EXPLAIN"> command shows how row-security policy
+ works on the supplied query.
+ <screen>
+ postgres=> EXPLAIN(costs off) SELECT * FROM customer WHERE f_leak(upasswd);
+ QUERY PLAN
+ --------------------------------------------
+ Subquery Scan on customer
+ Filter: f_leak(customer.upasswd)
+ -> Seq Scan on customer customer_1
+ Filter: ("current_user"() = uname)
+ (4 rows)
+ </screen>
+ This query execution plan means the preconfigured row-security policy is
+ implicitly added, and scan plan on the target relation being wrapped up
+ with a sub-query.
+ It ensures user given qualifiers, including functions with side effects,
+ are never executed earlier than the row-security policy regardless of
+ its cost, except for the cases when these were fully leakproof.
+ This design helps to tackle the scenario described in
+ <xref linkend="RULES-PRIVILEGES">; that introduces the order to evaluate
+ qualifiers is significant to keep confidentiality of invisible rows.
+ </para>
+
+ <para>
+ On the other hand, this design allows superusers to bypass checks with
+ row-security.
+ It ensures <application>pg_dump</application> can obtain a complete set
+ of database backup, and avoid to execute Trojan horse trap, being injected
+ as a row-security policy of user-defined table, with privileges of
+ superuser.
+ </para>
+
+ <para>
+ In case of queries on inherited tables, row-security policy of the parent
+ relation is not applied to child relations. Scope of the row-security
+ policy is limited to the relation on which it is set.
+ <screen>
+ postgres=> EXPLAIN(costs off) SELECT * FROM t1 WHERE f_leak(y);
+ QUERY PLAN
+ -------------------------------------
+ Append
+ -> Subquery Scan on t1
+ Filter: f_leak(t1.y)
+ -> Seq Scan on t1 t1_1
+ Filter: ((x % 2) = 0)
+ -> Seq Scan on t2
+ Filter: f_leak(y)
+ -> Subquery Scan on t3
+ Filter: f_leak(t3.y)
+ -> Seq Scan on t3 t3_1
+ Filter: ((x % 2) = 1)
+ (11 rows)
+ </screen>
+ In the above example, <literal>t1</literal> has inherited
+ child table <literal>t2</literal> and <literal>t3</literal>,
+ and row-security policy is set on only <literal>t1</literal>,
+ and <literal>t3</literal>, not <literal>t2</literal>.
+
+ The row-security policy of <literal>t1</literal>, <literal>x</literal>
+ must be even-number, is appended only <literal>t1</literal>,
+ neither <literal>t2</literal> nor <literal>t3</literal>.
+ On the contrary, <literal>t3</literal> has different row-security policy;
+ <literal>x</literal> must be odd-number.
+ </para>
+
+ <para>
+ Row-security feature also works to queries for writer-operations; such as
+ <xref linkend="SQL-INSERT">, <xref linkend="SQL-UPDATE"> or
+ <xref linkend="SQL-DELETE"> commands.
+ It ensures all the modified rows satisfies configured row-security policy.
+ The below query tries to update e-mail address of the
+ <literal>customer</> table, and configured row-security makes sure all the
+ modified rows's <literal>uname</> field has to match with
+ <literal>current_user</>.
+ </para>
+ <screen>
+ postgres=> EXPLAIN (costs off) UPDATE customer SET email = 'alice@example.com';
+ QUERY PLAN
+ --------------------------------------------------
+ Update on customer
+ -> Subquery Scan on customer
+ -> Seq Scan on customer customer_1
+ Filter: ("current_user"() = uname)
+ (4 rows)
+ </screen>
+ <para>
+ Modification of a certain table is consist of two different stuffs; one
+ is fetch rows to be modified from the result relation (except for
+ <literal>INSERT</> command), second is insertion of rows to the
+ result relation. Usually, before-row trigger or check constraints are
+ run in the second phase. In addition, row-security policy is also checked
+ on this stage, to prevent to insert or update rows with unprivileged
+ values.
+ </para>
+
+ <para>
+ Unlike other commercial database systems, we don't have any plan to allow
+ individual row-security policy for each command type. Even if we want to
+ perform with difference policy between <xref linkend="SQL-SELECT"> and
+ <xref linkend="SQL-DELETE">, <literal>RETURNING</> clause can leak the rows
+ to be invisible using <xref linkend="SQL-DELETE"> command.
+ </para>
+
+ <para>
+ Even though it is not a specific matter in row-security, please be careful
+ if you plan to use <literal>current_user</> in row-level security policy.
+ <xref linkend="SQL-DECLARE"> and <xref linkend="SQL-FETCH"> allows to
+ switch current user identifier during execution of the query.
+ Thus, it can fetch the first 100 rows with privilege of <literal>alice</>,
+ then remaining rows with privilege of <literal>bob</>. If and when query
+ execution plan contains some kind of materialization and row-security
+ policy contains <literal>current_user</>, the fetched tuples in
+ <literal>bob</>'s screen might be evaluated according to the privilege of
+ <literal>alice</>.
+ </para>
+ </sect1>
</chapter>
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
new file mode 100644
index c4d3f3c..9b4e9f5
*** a/src/backend/catalog/Makefile
--- b/src/backend/catalog/Makefile
*************** OBJS = catalog.o dependency.o heap.o ind
*** 15,21 ****
pg_constraint.o pg_conversion.o \
pg_depend.o pg_enum.o pg_inherits.o pg_largeobject.o pg_namespace.o \
pg_operator.o pg_proc.o pg_range.o pg_db_role_setting.o pg_shdepend.o \
! pg_type.o storage.o toasting.o
BKIFILES = postgres.bki postgres.description postgres.shdescription
--- 15,21 ----
pg_constraint.o pg_conversion.o \
pg_depend.o pg_enum.o pg_inherits.o pg_largeobject.o pg_namespace.o \
pg_operator.o pg_proc.o pg_range.o pg_db_role_setting.o pg_shdepend.o \
! pg_rowsecurity.o pg_type.o storage.o toasting.o
BKIFILES = postgres.bki postgres.description postgres.shdescription
*************** POSTGRES_BKI_SRCS = $(addprefix $(top_sr
*** 39,45 ****
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_default_acl.h pg_seclabel.h pg_shseclabel.h pg_collation.h pg_range.h \
toasting.h indexing.h \
)
--- 39,45 ----
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_rowsecurity.h \
pg_default_acl.h pg_seclabel.h pg_shseclabel.h pg_collation.h pg_range.h \
toasting.h indexing.h \
)
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
new file mode 100644
index fe17c96..d13b2e1
*** a/src/backend/catalog/dependency.c
--- b/src/backend/catalog/dependency.c
***************
*** 45,50 ****
--- 45,51 ----
#include "catalog/pg_opfamily.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_rewrite.h"
+ #include "catalog/pg_rowsecurity.h"
#include "catalog/pg_tablespace.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_ts_config.h"
*************** doDeletion(const ObjectAddress *object,
*** 1249,1254 ****
--- 1250,1259 ----
RemoveEventTriggerById(object->objectId);
break;
+ case OCLASS_ROWSECURITY:
+ RemoveRowSecurityById(object->objectId);
+ break;
+
default:
elog(ERROR, "unrecognized object class: %u",
object->classId);
*************** getObjectClass(const ObjectAddress *obje
*** 2308,2313 ****
--- 2313,2321 ----
case EventTriggerRelationId:
return OCLASS_EVENT_TRIGGER;
+
+ case RowSecurityRelationId:
+ return OCLASS_ROWSECURITY;
}
/* shouldn't get here */
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
new file mode 100644
index 64ca312..7eb7a17
*** a/src/backend/catalog/heap.c
--- b/src/backend/catalog/heap.c
*************** InsertPgClassTuple(Relation pg_class_des
*** 791,796 ****
--- 791,797 ----
values[Anum_pg_class_relhaspkey - 1] = BoolGetDatum(rd_rel->relhaspkey);
values[Anum_pg_class_relhasrules - 1] = BoolGetDatum(rd_rel->relhasrules);
values[Anum_pg_class_relhastriggers - 1] = BoolGetDatum(rd_rel->relhastriggers);
+ values[Anum_pg_class_relhasrowsecurity - 1] = BoolGetDatum(rd_rel->relhasrowsecurity);
values[Anum_pg_class_relhassubclass - 1] = BoolGetDatum(rd_rel->relhassubclass);
values[Anum_pg_class_relispopulated - 1] = BoolGetDatum(rd_rel->relispopulated);
values[Anum_pg_class_relfrozenxid - 1] = TransactionIdGetDatum(rd_rel->relfrozenxid);
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
new file mode 100644
index 4d22f3a..b4bc9d8
*** a/src/backend/catalog/objectaddress.c
--- b/src/backend/catalog/objectaddress.c
*************** getObjectDescription(const ObjectAddress
*** 2143,2148 ****
--- 2143,2198 ----
break;
}
+ case OCLASS_ROWSECURITY:
+ {
+ Relation rsec_rel;
+ ScanKeyData skey;
+ SysScanDesc sscan;
+ HeapTuple tuple;
+ Form_pg_rowsecurity form_rsec;
+
+ rsec_rel = heap_open(RowSecurityRelationId, AccessShareLock);
+
+ ScanKeyInit(&skey,
+ ObjectIdAttributeNumber,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(object->objectId));
+ sscan = systable_beginscan(rsec_rel, RowSecurityOidIndexId,
+ true, SnapshotNow, 1, &skey);
+ tuple = systable_getnext(sscan);
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "could not find tuple for row-security %u",
+ object->objectId);
+ form_rsec = (Form_pg_rowsecurity) GETSTRUCT(tuple);
+
+ appendStringInfo(&buffer, _("row-security of "));
+ getRelationDescription(&buffer, form_rsec->rsecrelid);
+ switch (form_rsec->rseccmd)
+ {
+ case ROWSECURITY_CMD_ALL:
+ appendStringInfo(&buffer, _(" FOR ALL"));
+ break;
+ case ROWSECURITY_CMD_SELECT:
+ appendStringInfo(&buffer, _(" FOR SELECT"));
+ break;
+ case ROWSECURITY_CMD_INSERT:
+ appendStringInfo(&buffer, _(" FOR INSERT"));
+ break;
+ case ROWSECURITY_CMD_UPDATE:
+ appendStringInfo(&buffer, _(" FOR UPDATE"));
+ break;
+ case ROWSECURITY_CMD_DELETE:
+ appendStringInfo(&buffer, _(" FOR DELETE"));
+ break;
+ default:
+ elog(ERROR, "unexpected row-security command type: %c",
+ form_rsec->rseccmd);
+ }
+ systable_endscan(sscan);
+ heap_close(rsec_rel, AccessShareLock);
+ break;
+ }
+
default:
appendStringInfo(&buffer, "unrecognized object %u %u %d",
object->classId,
diff --git a/src/backend/catalog/pg_rowsecurity.c b/src/backend/catalog/pg_rowsecurity.c
new file mode 100644
index ...25739ff
*** a/src/backend/catalog/pg_rowsecurity.c
--- b/src/backend/catalog/pg_rowsecurity.c
***************
*** 0 ****
--- 1,337 ----
+ /* -------------------------------------------------------------------------
+ *
+ * pg_rowsecurity.c
+ * routines to support manipulation of the pg_rowsecurity catalog
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * -------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+ #include "access/genam.h"
+ #include "access/heapam.h"
+ #include "access/htup_details.h"
+ #include "access/sysattr.h"
+ #include "catalog/dependency.h"
+ #include "catalog/indexing.h"
+ #include "catalog/pg_class.h"
+ #include "catalog/pg_rowsecurity.h"
+ #include "catalog/pg_type.h"
+ #include "nodes/nodeFuncs.h"
+ #include "optimizer/clauses.h"
+ #include "parser/parse_clause.h"
+ #include "parser/parse_node.h"
+ #include "parser/parse_relation.h"
+ #include "utils/builtins.h"
+ #include "utils/fmgroids.h"
+ #include "utils/inval.h"
+ #include "utils/rel.h"
+ #include "utils/syscache.h"
+ #include "utils/tqual.h"
+
+ /*
+ * Load row-security policy from the catalog, and keep it on
+ * the relation cache.
+ */
+ void
+ RelationBuildRowSecurity(Relation relation)
+ {
+ Relation catalog;
+ ScanKeyData skey;
+ SysScanDesc sscan;
+ HeapTuple tuple;
+ MemoryContext oldcxt;
+ MemoryContext rscxt = NULL;
+ RowSecurityDesc *rsdesc = NULL;
+
+ catalog = heap_open(RowSecurityRelationId, AccessShareLock);
+
+ ScanKeyInit(&skey,
+ Anum_pg_rowsecurity_rsecrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(RelationGetRelid(relation)));
+ sscan = systable_beginscan(catalog, RowSecurityRelidIndexId, true,
+ SnapshotNow, 1, &skey);
+ PG_TRY();
+ {
+ while (HeapTupleIsValid(tuple = systable_getnext(sscan)))
+ {
+ Datum value;
+ bool isnull;
+ char *temp;
+
+ if (!rsdesc)
+ {
+ rscxt = AllocSetContextCreate(CacheMemoryContext,
+ "Row-security descriptor",
+ ALLOCSET_SMALL_MINSIZE,
+ ALLOCSET_SMALL_INITSIZE,
+ ALLOCSET_SMALL_MAXSIZE);
+ oldcxt = MemoryContextSwitchTo(rscxt);
+ rsdesc = palloc0(sizeof(RowSecurityDesc));
+ rsdesc->rscxt = rscxt;
+ MemoryContextSwitchTo(oldcxt);
+ }
+ value = heap_getattr(tuple, Anum_pg_rowsecurity_rseccmd,
+ RelationGetDescr(catalog), &isnull);
+ Assert(!isnull);
+
+ if (DatumGetChar(value) != ROWSECURITY_CMD_ALL)
+ {
+ ereport(WARNING,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("Per-command row-security not implemented")));
+ continue;
+ }
+
+ value = heap_getattr(tuple, Anum_pg_rowsecurity_rsecqual,
+ RelationGetDescr(catalog), &isnull);
+ Assert(!isnull);
+ temp = TextDatumGetCString(value);
+
+ oldcxt = MemoryContextSwitchTo(rscxt);
+ rsdesc->rsall.rsecid = HeapTupleGetOid(tuple);
+ rsdesc->rsall.qual = (Expr *) stringToNode(temp);
+ Assert(exprType((Node *)rsdesc->rsall.qual) == BOOLOID);
+ rsdesc->rsall.hassublinks
+ = contain_subplans((Node *)rsdesc->rsall.qual);
+ MemoryContextSwitchTo(oldcxt);
+
+ pfree(temp);
+ }
+ }
+ PG_CATCH();
+ {
+ if (rscxt != NULL)
+ MemoryContextDelete(rscxt);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ systable_endscan(sscan);
+ heap_close(catalog, AccessShareLock);
+
+ relation->rsdesc = rsdesc;
+ }
+
+ /*
+ * Parse the supplied row-security policy, and insert/update a row
+ * of pg_rowsecurity catalog.
+ */
+ static void
+ InsertOrUpdatePolicyRow(Relation relation, char rseccmd, Node *clause)
+ {
+ Oid relationId = RelationGetRelid(relation);
+ Oid rowsecId;
+ ParseState *pstate;
+ RangeTblEntry *rte;
+ Node *qual;
+ Relation catalog;
+ ScanKeyData skeys[2];
+ SysScanDesc sscan;
+ HeapTuple oldtup;
+ HeapTuple newtup;
+ Datum values[Natts_pg_rowsecurity];
+ bool isnull[Natts_pg_rowsecurity];
+ bool replaces[Natts_pg_rowsecurity];
+ ObjectAddress target;
+ ObjectAddress myself;
+
+ /* Parse the supplied clause */
+ pstate = make_parsestate(NULL);
+
+ rte = addRangeTableEntryForRelation(pstate, relation,
+ NULL, false, false);
+ addRTEtoQuery(pstate, rte, false, true, true);
+
+ qual = transformWhereClause(pstate, copyObject(clause),
+ EXPR_KIND_ROW_SECURITY,
+ "ROW SECURITY");
+ /* zero-clear */
+ memset(values, 0, sizeof(values));
+ memset(replaces, 0, sizeof(replaces));
+ memset(isnull, 0, sizeof(isnull));
+
+ /* Update or Insert an entry to pg_rowsecurity catalog */
+ catalog = heap_open(RowSecurityRelationId, RowExclusiveLock);
+
+ ScanKeyInit(&skeys[0],
+ Anum_pg_rowsecurity_rsecrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(RelationGetRelid(relation)));
+ ScanKeyInit(&skeys[1],
+ Anum_pg_rowsecurity_rseccmd,
+ BTEqualStrategyNumber, F_CHAREQ,
+ CharGetDatum(rseccmd));
+ sscan = systable_beginscan(catalog, RowSecurityRelidIndexId, true,
+ SnapshotNow, 2, skeys);
+ oldtup = systable_getnext(sscan);
+ if (HeapTupleIsValid(oldtup))
+ {
+ rowsecId = HeapTupleGetOid(oldtup);
+
+ replaces[Anum_pg_rowsecurity_rsecqual - 1] = true;
+ values[Anum_pg_rowsecurity_rsecqual - 1]
+ = CStringGetTextDatum(nodeToString(qual));
+
+ newtup = heap_modify_tuple(oldtup,
+ RelationGetDescr(catalog),
+ values, isnull, replaces);
+ simple_heap_update(catalog, &newtup->t_self, newtup);
+
+ deleteDependencyRecordsFor(RowSecurityRelationId, rowsecId, false);
+ }
+ else
+ {
+ values[Anum_pg_rowsecurity_rsecrelid - 1]
+ = ObjectIdGetDatum(relationId);
+ values[Anum_pg_rowsecurity_rseccmd - 1]
+ = CharGetDatum(rseccmd);
+ values[Anum_pg_rowsecurity_rsecqual - 1]
+ = CStringGetTextDatum(nodeToString(qual));
+ newtup = heap_form_tuple(RelationGetDescr(catalog),
+ values, isnull);
+ rowsecId = simple_heap_insert(catalog, newtup);
+ }
+ CatalogUpdateIndexes(catalog, newtup);
+
+ heap_freetuple(newtup);
+
+ /* records dependencies of row-security policy and relation/columns */
+ target.classId = RelationRelationId;
+ target.objectId = relationId;
+ target.objectSubId = 0;
+
+ myself.classId = RowSecurityRelationId;
+ myself.objectId = rowsecId;
+ myself.objectSubId = 0;
+
+ recordDependencyOn(&myself, &target, DEPENDENCY_AUTO);
+
+ recordDependencyOnExpr(&myself, qual, pstate->p_rtable,
+ DEPENDENCY_NORMAL);
+ free_parsestate(pstate);
+
+ systable_endscan(sscan);
+ heap_close(catalog, RowExclusiveLock);
+ }
+
+ /*
+ * Remove row-security policy row of pg_rowsecurity
+ */
+ static void
+ DeletePolicyRow(Relation relation, char rseccmd)
+ {
+ Assert(rseccmd == ROWSECURITY_CMD_ALL);
+
+ if (relation->rsdesc)
+ {
+ ObjectAddress address;
+
+ address.classId = RowSecurityRelationId;
+ address.objectId = relation->rsdesc->rsall.rsecid;
+ address.objectSubId = 0;
+
+ performDeletion(&address, DROP_RESTRICT, 0);
+ }
+ else
+ {
+ /* Nothing to do here */
+ elog(INFO, "relation %s has no row-security policy, skipped",
+ RelationGetRelationName(relation));
+ }
+ }
+
+ /*
+ * Guts of row-security policy deletion.
+ */
+ void
+ RemoveRowSecurityById(Oid rowsecId)
+ {
+ Relation catalog;
+ ScanKeyData skey;
+ SysScanDesc sscan;
+ HeapTuple tuple;
+ Relation rel;
+ Oid relid;
+
+ catalog = heap_open(RowSecurityRelationId, RowExclusiveLock);
+
+ ScanKeyInit(&skey,
+ ObjectIdAttributeNumber,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(rowsecId));
+ sscan = systable_beginscan(catalog, RowSecurityOidIndexId, true,
+ SnapshotNow, 1, &skey);
+ tuple = systable_getnext(sscan);
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "could not find tuple for row-security %u", rowsecId);
+
+ /*
+ * Open and exclusive-lock the relation the row-security belongs to.
+ */
+ relid = ((Form_pg_rowsecurity) GETSTRUCT(tuple))->rsecrelid;
+
+ rel = heap_open(relid, AccessExclusiveLock);
+
+ simple_heap_delete(catalog, &tuple->t_self);
+
+ /* Ensure relcache entries of other session being rebuilt */
+ CacheInvalidateRelcache(rel);
+
+ heap_close(rel, NoLock);
+
+ systable_endscan(sscan);
+ heap_close(catalog, RowExclusiveLock);
+ }
+
+ /*
+ * ALTER TABLE <name> SET ROW SECURITY (...) OR
+ * RESET ROW SECURITY
+ */
+ void
+ ATExecSetRowSecurity(Relation relation, const char *cmdname, Node *clause)
+ {
+ Oid relid = RelationGetRelid(relation);
+ char rseccmd;
+
+ if (strcmp(cmdname, "all") == 0)
+ rseccmd = ROWSECURITY_CMD_ALL;
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("Row-security for \"%s\" is not implemented yet",
+ cmdname)));
+
+ if (clause != NULL)
+ {
+ InsertOrUpdatePolicyRow(relation, rseccmd, clause);
+
+ /*
+ * Also, turn on relhasrowsecurity, if not.
+ */
+ if (!RelationGetForm(relation)->relhasrowsecurity)
+ {
+ Relation class_rel = heap_open(RelationRelationId,
+ RowExclusiveLock);
+ HeapTuple tuple;
+
+ tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for relation %u", relid);
+
+ ((Form_pg_class) GETSTRUCT(tuple))->relhasrowsecurity = true;
+
+ simple_heap_update(class_rel, &tuple->t_self, tuple);
+ CatalogUpdateIndexes(class_rel, tuple);
+
+ heap_freetuple(tuple);
+ heap_close(class_rel, RowExclusiveLock);
+ }
+ }
+ else
+ DeletePolicyRow(relation, rseccmd);
+
+ CacheInvalidateRelcache(relation);
+ }
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
new file mode 100644
index 31819cc..9cb8f1b
*** a/src/backend/commands/copy.c
--- b/src/backend/commands/copy.c
***************
*** 24,29 ****
--- 24,30 ----
#include "access/htup_details.h"
#include "access/sysattr.h"
#include "access/xact.h"
+ #include "catalog/heap.h"
#include "catalog/namespace.h"
#include "catalog/pg_type.h"
#include "commands/copy.h"
***************
*** 34,48 ****
--- 35,53 ----
#include "libpq/pqformat.h"
#include "mb/pg_wchar.h"
#include "miscadmin.h"
+ #include "nodes/makefuncs.h"
#include "optimizer/clauses.h"
#include "optimizer/planner.h"
+ #include "optimizer/rowsecurity.h"
#include "parser/parse_relation.h"
+ #include "parser/parsetree.h"
#include "rewrite/rewriteHandler.h"
#include "storage/fd.h"
#include "tcop/tcopprot.h"
#include "utils/acl.h"
#include "utils/builtins.h"
#include "utils/lsyscache.h"
+ #include "utils/syscache.h"
#include "utils/memutils.h"
#include "utils/portal.h"
#include "utils/rel.h"
*************** DoCopy(const CopyStmt *stmt, const char
*** 814,819 ****
--- 819,839 ----
rel = heap_openrv(stmt->relation,
(is_from ? RowExclusiveLock : AccessShareLock));
+ tupDesc = RelationGetDescr(rel);
+ attnums = CopyGetAttnums(tupDesc, rel, stmt->attlist);
+
+ /*
+ * We have to run regular query, if the target relation has
+ * row-level security policy
+ */
+ if (copy_row_security_policy((CopyStmt *)stmt, rel, attnums))
+ {
+ heap_close(rel, NoLock); /* close with keeping lock */
+ relid = InvalidOid;
+ rel = NULL;
+ }
+ else
+ {
relid = RelationGetRelid(rel);
rte = makeNode(RangeTblEntry);
*************** DoCopy(const CopyStmt *stmt, const char
*** 822,829 ****
rte->relkind = rel->rd_rel->relkind;
rte->requiredPerms = required_access;
- tupDesc = RelationGetDescr(rel);
- attnums = CopyGetAttnums(tupDesc, rel, stmt->attlist);
foreach(cur, attnums)
{
int attno = lfirst_int(cur) -
--- 842,847 ----
*************** DoCopy(const CopyStmt *stmt, const char
*** 835,840 ****
--- 853,859 ----
rte->selectedCols = bms_add_member(rte->selectedCols, attno);
}
ExecCheckRTPerms(list_make1(rte), true);
+ }
}
else
{
*************** ProcessCopyOptions(CopyState cstate,
*** 1193,1198 ****
--- 1212,1264 ----
}
/*
+ * Adjust Query tree constructed with row-level security feature.
+ * If WITH OIDS option was supplied, it adds Var node to reference
+ * object-id system column.
+ */
+ static void
+ fixup_oid_of_rls_query(Query *query)
+ {
+ RangeTblEntry *subrte;
+ TargetEntry *subtle;
+ Var *subvar;
+ ListCell *cell;
+ Form_pg_attribute attform
+ = SystemAttributeDefinition(ObjectIdAttributeNumber, true);
+
+ subrte = rt_fetch((Index) 1, query->rtable);
+ Assert(subrte->rtekind == RTE_RELATION);
+
+ if (!SearchSysCacheExists2(ATTNUM,
+ ObjectIdGetDatum(subrte->relid),
+ Int16GetDatum(attform->attnum)))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_COLUMN),
+ errmsg("table \"%s\" does not have OIDs",
+ get_rel_name(subrte->relid))));
+
+ subvar = makeVar((Index) 1,
+ attform->attnum,
+ attform->atttypid,
+ attform->atttypmod,
+ attform->attcollation,
+ 0);
+ subtle = makeTargetEntry((Expr *) subvar,
+ 0,
+ pstrdup(NameStr(attform->attname)),
+ false);
+
+ query->targetList = list_concat(list_make1(subtle),
+ query->targetList);
+ /* adjust resno of TargetEntry */
+ foreach (cell, query->targetList)
+ {
+ subtle = lfirst(cell);
+ subtle->resno++;
+ }
+ }
+
+ /*
* Common setup routines used by BeginCopyFrom and BeginCopyTo.
*
* Iff <binary>, unload or reload in the binary format, as opposed to the
*************** BeginCopy(bool is_from,
*** 1264,1269 ****
--- 1330,1354 ----
Assert(!is_from);
cstate->rel = NULL;
+ /*
+ * In case when regular COPY TO was replaced because of row-level
+ * security, "raw_query" node have already analyzed / rewritten
+ * query tree.
+ */
+ if (IsA(raw_query, Query))
+ {
+ query = (Query *) raw_query;
+
+ Assert(query->querySource == QSRC_ROW_SECURITY);
+ if (cstate->oids)
+ {
+ fixup_oid_of_rls_query(query);
+ cstate->oids = false;
+ }
+ attnamelist = NIL;
+ }
+ else
+ {
/* Don't allow COPY w/ OIDs from a select */
if (cstate->oids)
ereport(ERROR,
*************** BeginCopy(bool is_from,
*** 1288,1293 ****
--- 1373,1379 ----
elog(ERROR, "unexpected rewrite result");
query = (Query *) linitial(rewritten);
+ }
/* The grammar allows SELECT INTO, but we don't support that */
if (query->utilityStmt != NULL &&
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
new file mode 100644
index 328e2a8..0374629
*** a/src/backend/commands/event_trigger.c
--- b/src/backend/commands/event_trigger.c
*************** EventTriggerSupportsObjectClass(ObjectCl
*** 992,997 ****
--- 992,998 ----
case OCLASS_USER_MAPPING:
case OCLASS_DEFACL:
case OCLASS_EXTENSION:
+ case OCLASS_ROWSECURITY:
return true;
case MAX_OCLASS:
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
new file mode 100644
index cb87d90..79f3ff4
*** a/src/backend/commands/tablecmds.c
--- b/src/backend/commands/tablecmds.c
***************
*** 37,42 ****
--- 37,43 ----
#include "catalog/pg_inherits_fn.h"
#include "catalog/pg_namespace.h"
#include "catalog/pg_opclass.h"
+ #include "catalog/pg_rowsecurity.h"
#include "catalog/pg_tablespace.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
*************** AlterTableGetLockLevel(List *cmds)
*** 2787,2792 ****
--- 2788,2795 ----
case AT_SetTableSpace: /* must rewrite heap */
case AT_DropNotNull: /* may change some SQL plans */
case AT_SetNotNull:
+ case AT_SetRowSecurity:
+ case AT_ResetRowSecurity:
case AT_GenericOptions:
case AT_AlterColumnGenericOptions:
cmd_lockmode = AccessExclusiveLock;
*************** ATPrepCmd(List **wqueue, Relation rel, A
*** 3155,3160 ****
--- 3158,3165 ----
case AT_DropInherit: /* NO INHERIT */
case AT_AddOf: /* OF */
case AT_DropOf: /* NOT OF */
+ case AT_SetRowSecurity:
+ case AT_ResetRowSecurity:
ATSimplePermissions(rel, ATT_TABLE);
/* These commands never recurse */
/* No command-specific prep needed */
*************** ATExecCmd(List **wqueue, AlteredTableInf
*** 3440,3445 ****
--- 3445,3456 ----
case AT_DropOf:
ATExecDropOf(rel, lockmode);
break;
+ case AT_SetRowSecurity:
+ ATExecSetRowSecurity(rel, cmd->name, (Node *) cmd->def);
+ break;
+ case AT_ResetRowSecurity:
+ ATExecSetRowSecurity(rel, cmd->name, NULL);
+ break;
case AT_GenericOptions:
ATExecGenericOptions(rel, (List *) cmd->def);
break;
*************** ATExecAlterColumnType(AlteredTableInfo *
*** 7789,7794 ****
--- 7800,7821 ----
Assert(defaultexpr);
break;
+ case OCLASS_ROWSECURITY:
+ /*
+ * A row-level security policy can depend on a column in case
+ * when the policy clause references a particular column.
+ * Due to same reason why TRIGGER ... WHEN does not support
+ * to change column's type being referenced in clause, row-
+ * level security policy also does not support it.
+ */
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot alter type of a column used in a row-level security policy"),
+ errdetail("%s depends on column \"%s\"",
+ getObjectDescription(&foundObject),
+ colName)));
+ break;
+
case OCLASS_PROC:
case OCLASS_TYPE:
case OCLASS_CAST:
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
new file mode 100644
index bcc6496..5bafcb6
*** a/src/backend/nodes/copyfuncs.c
--- b/src/backend/nodes/copyfuncs.c
*************** _copyAppendRelInfo(const AppendRelInfo *
*** 1936,1941 ****
--- 1936,1942 ----
COPY_SCALAR_FIELD(parent_relid);
COPY_SCALAR_FIELD(child_relid);
+ COPY_SCALAR_FIELD(child_result);
COPY_SCALAR_FIELD(parent_reltype);
COPY_SCALAR_FIELD(child_reltype);
COPY_NODE_FIELD(translated_vars);
*************** _copyRangeTblEntry(const RangeTblEntry *
*** 1977,1982 ****
--- 1978,1984 ----
COPY_SCALAR_FIELD(relkind);
COPY_NODE_FIELD(subquery);
COPY_SCALAR_FIELD(security_barrier);
+ COPY_SCALAR_FIELD(rowsec_relid);
COPY_SCALAR_FIELD(jointype);
COPY_NODE_FIELD(joinaliasvars);
COPY_NODE_FIELD(funcexpr);
*************** _copyQuery(const Query *from)
*** 2448,2453 ****
--- 2450,2456 ----
COPY_SCALAR_FIELD(canSetTag);
COPY_NODE_FIELD(utilityStmt);
COPY_SCALAR_FIELD(resultRelation);
+ COPY_SCALAR_FIELD(sourceRelation);
COPY_SCALAR_FIELD(hasAggs);
COPY_SCALAR_FIELD(hasWindowFuncs);
COPY_SCALAR_FIELD(hasSubLinks);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
new file mode 100644
index 7f9737e..7fd8f3d
*** a/src/backend/nodes/equalfuncs.c
--- b/src/backend/nodes/equalfuncs.c
*************** _equalAppendRelInfo(const AppendRelInfo
*** 807,812 ****
--- 807,813 ----
{
COMPARE_SCALAR_FIELD(parent_relid);
COMPARE_SCALAR_FIELD(child_relid);
+ COMPARE_SCALAR_FIELD(child_result);
COMPARE_SCALAR_FIELD(parent_reltype);
COMPARE_SCALAR_FIELD(child_reltype);
COMPARE_NODE_FIELD(translated_vars);
*************** _equalQuery(const Query *a, const Query
*** 842,847 ****
--- 843,849 ----
COMPARE_SCALAR_FIELD(canSetTag);
COMPARE_NODE_FIELD(utilityStmt);
COMPARE_SCALAR_FIELD(resultRelation);
+ COMPARE_SCALAR_FIELD(sourceRelation);
COMPARE_SCALAR_FIELD(hasAggs);
COMPARE_SCALAR_FIELD(hasWindowFuncs);
COMPARE_SCALAR_FIELD(hasSubLinks);
*************** _equalRangeTblEntry(const RangeTblEntry
*** 2228,2233 ****
--- 2230,2236 ----
COMPARE_SCALAR_FIELD(relkind);
COMPARE_NODE_FIELD(subquery);
COMPARE_SCALAR_FIELD(security_barrier);
+ COMPARE_SCALAR_FIELD(rowsec_relid);
COMPARE_SCALAR_FIELD(jointype);
COMPARE_NODE_FIELD(joinaliasvars);
COMPARE_NODE_FIELD(funcexpr);
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
new file mode 100644
index a896d76..2ed792f
*** a/src/backend/nodes/nodeFuncs.c
--- b/src/backend/nodes/nodeFuncs.c
*************** query_tree_walker(Query *query,
*** 1877,1884 ****
return true;
if (walker((Node *) query->withCheckOptions, context))
return true;
! if (walker((Node *) query->returningList, context))
! return true;
if (walker((Node *) query->jointree, context))
return true;
if (walker(query->setOperations, context))
--- 1877,1887 ----
return true;
if (walker((Node *) query->withCheckOptions, context))
return true;
! if (!(flags & QTW_IGNORE_RETURNING))
! {
! if (walker((Node *) query->returningList, context))
! return true;
! }
if (walker((Node *) query->jointree, context))
return true;
if (walker(query->setOperations, context))
*************** query_tree_mutator(Query *query,
*** 2603,2609 ****
MUTATE(query->targetList, query->targetList, List *);
MUTATE(query->withCheckOptions, query->withCheckOptions, List *);
! MUTATE(query->returningList, query->returningList, List *);
MUTATE(query->jointree, query->jointree, FromExpr *);
MUTATE(query->setOperations, query->setOperations, Node *);
MUTATE(query->havingQual, query->havingQual, Node *);
--- 2606,2615 ----
MUTATE(query->targetList, query->targetList, List *);
MUTATE(query->withCheckOptions, query->withCheckOptions, List *);
! if (!(flags & QTW_IGNORE_RETURNING))
! MUTATE(query->returningList, query->returningList, List *);
! else
! query->returningList = copyObject(query->returningList);
MUTATE(query->jointree, query->jointree, FromExpr *);
MUTATE(query->setOperations, query->setOperations, Node *);
MUTATE(query->havingQual, query->havingQual, Node *);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
new file mode 100644
index 48cd9dc..5b4bbae
*** a/src/backend/nodes/outfuncs.c
--- b/src/backend/nodes/outfuncs.c
*************** _outAppendRelInfo(StringInfo str, const
*** 1923,1928 ****
--- 1923,1929 ----
WRITE_UINT_FIELD(parent_relid);
WRITE_UINT_FIELD(child_relid);
+ WRITE_UINT_FIELD(child_result);
WRITE_OID_FIELD(parent_reltype);
WRITE_OID_FIELD(child_reltype);
WRITE_NODE_FIELD(translated_vars);
*************** _outQuery(StringInfo str, const Query *n
*** 2237,2242 ****
--- 2238,2244 ----
appendStringInfo(str, " :utilityStmt <>");
WRITE_INT_FIELD(resultRelation);
+ WRITE_INT_FIELD(sourceRelation);
WRITE_BOOL_FIELD(hasAggs);
WRITE_BOOL_FIELD(hasWindowFuncs);
WRITE_BOOL_FIELD(hasSubLinks);
*************** _outRangeTblEntry(StringInfo str, const
*** 2372,2377 ****
--- 2374,2380 ----
case RTE_SUBQUERY:
WRITE_NODE_FIELD(subquery);
WRITE_BOOL_FIELD(security_barrier);
+ WRITE_OID_FIELD(rowsec_relid);
break;
case RTE_JOIN:
WRITE_ENUM_FIELD(jointype, JoinType);
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
new file mode 100644
index dc9cb3e..6c1af68
*** a/src/backend/nodes/readfuncs.c
--- b/src/backend/nodes/readfuncs.c
*************** _readQuery(void)
*** 199,204 ****
--- 199,205 ----
READ_BOOL_FIELD(canSetTag);
READ_NODE_FIELD(utilityStmt);
READ_INT_FIELD(resultRelation);
+ READ_INT_FIELD(sourceRelation);
READ_BOOL_FIELD(hasAggs);
READ_BOOL_FIELD(hasWindowFuncs);
READ_BOOL_FIELD(hasSubLinks);
*************** _readRangeTblEntry(void)
*** 1213,1218 ****
--- 1214,1220 ----
case RTE_SUBQUERY:
READ_NODE_FIELD(subquery);
READ_BOOL_FIELD(security_barrier);
+ READ_OID_FIELD(rowsec_relid);
break;
case RTE_JOIN:
READ_ENUM_FIELD(jointype, JoinType);
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
new file mode 100644
index 01e2fa3..1893516
*** a/src/backend/optimizer/plan/planner.c
--- b/src/backend/optimizer/plan/planner.c
***************
*** 33,38 ****
--- 33,39 ----
#include "optimizer/planmain.h"
#include "optimizer/planner.h"
#include "optimizer/prep.h"
+ #include "optimizer/rowsecurity.h"
#include "optimizer/subselect.h"
#include "optimizer/tlist.h"
#include "parser/analyze.h"
*************** standard_planner(Query *parse, int curso
*** 176,181 ****
--- 177,183 ----
glob->lastPHId = 0;
glob->lastRowMarkId = 0;
glob->transientPlan = false;
+ glob->planUserId = InvalidOid;
/* Determine what fraction of the plan is likely to be scanned */
if (cursorOptions & CURSOR_OPT_FAST_PLAN)
*************** standard_planner(Query *parse, int curso
*** 253,258 ****
--- 255,261 ----
result->relationOids = glob->relationOids;
result->invalItems = glob->invalItems;
result->nParamExec = glob->nParamExec;
+ result->planUserId = glob->planUserId;
return result;
}
*************** subquery_planner(PlannerGlobal *glob, Qu
*** 403,408 ****
--- 406,424 ----
expand_inherited_tables(root);
/*
+ * Apply row-security policy of the relation being referenced,
+ * if configured with either of built-in or extension's features.
+ * RangeTblEntry of the relation with row-security policy shall
+ * be replaced with a row-security subquery that has simple scan
+ * on the target relation with row-security policy qualifiers.
+ *
+ * This routine assumes PlannerInfo is already handled with
+ * expand_inherited_tables, thus, AppendRelInfo or PlanRowMark
+ * have valid information.
+ */
+ apply_row_security_policy(root);
+
+ /*
* Set hasHavingQual to remember if HAVING clause is present. Needed
* because preprocess_expression will reduce a constant-true condition to
* an empty qual list ... but "HAVING TRUE" is not a semantic no-op.
*************** inheritance_planner(PlannerInfo *root)
*** 887,892 ****
--- 903,910 ----
newrti = list_length(subroot.parse->rtable) + 1;
ChangeVarNodes((Node *) subroot.parse, rti, newrti, 0);
ChangeVarNodes((Node *) subroot.rowMarks, rti, newrti, 0);
+ if (subroot.parse->sourceRelation == rti)
+ subroot.parse->sourceRelation = newrti;
rte = copyObject(rte);
subroot.parse->rtable = lappend(subroot.parse->rtable,
rte);
*************** inheritance_planner(PlannerInfo *root)
*** 950,956 ****
root->init_plans = subroot.init_plans;
/* Build list of target-relation RT indexes */
! resultRelations = lappend_int(resultRelations, appinfo->child_relid);
/* Build lists of per-relation WCO and RETURNING targetlists */
if (parse->withCheckOptions)
--- 968,977 ----
root->init_plans = subroot.init_plans;
/* Build list of target-relation RT indexes */
! resultRelations = lappend_int(resultRelations,
! (appinfo->child_result > 0 ?
! appinfo->child_result :
! appinfo->child_relid));
/* Build lists of per-relation WCO and RETURNING targetlists */
if (parse->withCheckOptions)
diff --git a/src/backend/optimizer/prep/preptlist.c b/src/backend/optimizer/prep/preptlist.c
new file mode 100644
index fb67f9e..4abf434
*** a/src/backend/optimizer/prep/preptlist.c
--- b/src/backend/optimizer/prep/preptlist.c
***************
*** 37,44 ****
static List *expand_targetlist(List *tlist, int command_type,
! Index result_relation, List *range_table);
/*
* preprocess_targetlist
--- 37,84 ----
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 Var that references sub-queries being originated from regular
+ * relations with row-level security policy due to nature of sub-query
+ * that has no system-column.
+ */
+ static AttrNumber
+ lookup_varattno(AttrNumber attno, Index rt_index, List *rtables)
+ {
+ RangeTblEntry *rte = rt_fetch(rt_index, rtables);
+
+ if (rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_SECURITY)
+ {
+ ListCell *cell;
+
+ foreach (cell, rte->subquery->targetList)
+ {
+ TargetEntry *tle = lfirst(cell);
+ Var *var;
+
+ if (IsA(tle->expr, Const))
+ continue;
+
+ var = (Var *) tle->expr;
+ Assert(IsA(var, Var));
+
+ if (var->varattno == attno)
+ return tle->resno;
+ }
+ elog(ERROR, "invalid attno %d on row-security subquery target-list",
+ attno);
+ }
+ return attno;
+ }
/*
* preprocess_targetlist
*************** preprocess_targetlist(PlannerInfo *root,
*** 73,80 ****
* 10/94
*/
if (command_type == CMD_INSERT || command_type == CMD_UPDATE)
tlist = expand_targetlist(tlist, command_type,
! result_relation, range_table);
/*
* Add necessary junk columns for rowmarked rels. These values are needed
--- 113,125 ----
* 10/94
*/
if (command_type == CMD_INSERT || command_type == CMD_UPDATE)
+ {
+ Index source_relation = (parse->sourceRelation > 0 ?
+ parse->sourceRelation :
+ result_relation);
tlist = expand_targetlist(tlist, command_type,
! source_relation, range_table);
! }
/*
* Add necessary junk columns for rowmarked rels. These values are needed
*************** preprocess_targetlist(PlannerInfo *root,
*** 96,102 ****
{
/* It's a regular table, so fetch its TID */
var = makeVar(rc->rti,
! SelfItemPointerAttributeNumber,
TIDOID,
-1,
InvalidOid,
--- 141,148 ----
{
/* It's a regular table, so fetch its TID */
var = makeVar(rc->rti,
! lookup_varattno(SelfItemPointerAttributeNumber,
! rc->rti, range_table),
TIDOID,
-1,
InvalidOid,
*************** preprocess_targetlist(PlannerInfo *root,
*** 112,118 ****
if (rc->isParent)
{
var = makeVar(rc->rti,
! TableOidAttributeNumber,
OIDOID,
-1,
InvalidOid,
--- 158,165 ----
if (rc->isParent)
{
var = makeVar(rc->rti,
! lookup_varattno(TableOidAttributeNumber,
! rc->rti, range_table),
OIDOID,
-1,
InvalidOid,
*************** preprocess_targetlist(PlannerInfo *root,
*** 130,136 ****
/* Not a table, so we need the whole row as a junk var */
var = makeWholeRowVar(rt_fetch(rc->rti, range_table),
rc->rti,
! 0,
false);
snprintf(resname, sizeof(resname), "wholerow%u", rc->rowmarkId);
tle = makeTargetEntry((Expr *) var,
--- 177,183 ----
/* Not a table, so we need the whole row as a junk var */
var = makeWholeRowVar(rt_fetch(rc->rti, range_table),
rc->rti,
! lookup_varattno(0, rc->rti, range_table),
false);
snprintf(resname, sizeof(resname), "wholerow%u", rc->rowmarkId);
tle = makeTargetEntry((Expr *) var,
*************** expand_targetlist(List *tlist, int comma
*** 299,305 ****
if (!att_tup->attisdropped)
{
new_expr = (Node *) makeVar(result_relation,
! attrno,
atttype,
atttypmod,
attcollation,
--- 346,354 ----
if (!att_tup->attisdropped)
{
new_expr = (Node *) makeVar(result_relation,
! 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
new file mode 100644
index e249628..0cbb035
*** a/src/backend/optimizer/prep/prepunion.c
--- b/src/backend/optimizer/prep/prepunion.c
*************** typedef struct
*** 55,60 ****
--- 55,61 ----
{
PlannerInfo *root;
AppendRelInfo *appinfo;
+ bool in_returning;
} adjust_appendrel_attrs_context;
static Plan *recurse_set_operations(Node *setOp, PlannerInfo *root,
*************** adjust_appendrel_attrs(PlannerInfo *root
*** 1594,1599 ****
--- 1595,1601 ----
context.root = root;
context.appinfo = appinfo;
+ context.in_returning = false;
/*
* Must be prepared to start with a Query or a bare expression tree.
*************** adjust_appendrel_attrs(PlannerInfo *root
*** 1605,1614 ****
newnode = query_tree_mutator((Query *) node,
adjust_appendrel_attrs_mutator,
(void *) &context,
! QTW_IGNORE_RC_SUBQUERIES);
if (newnode->resultRelation == appinfo->parent_relid)
{
! newnode->resultRelation = appinfo->child_relid;
/* Fix tlist resnos too, if it's inherited UPDATE */
if (newnode->commandType == CMD_UPDATE)
newnode->targetList =
--- 1607,1635 ----
newnode = query_tree_mutator((Query *) node,
adjust_appendrel_attrs_mutator,
(void *) &context,
! QTW_IGNORE_RC_SUBQUERIES |
! QTW_IGNORE_RETURNING);
! /*
! * Returning clause on the relation being replaced with row-
! * security subquery shall be handled in a special way, because
! * of no system columns on subquery.
! * Var references to system column or whole-row reference need
! * to be adjusted to reference artificial columns on behalf of
! * the underlying these columns, however, RETURNGIN clause is
! * an exception because its Var nodes are evaluated towards
! * the "raw" target relation, not a fetched tuple.
! */
! context.in_returning = true;
! newnode->returningList = (List *)
! expression_tree_mutator((Node *) newnode->returningList,
! adjust_appendrel_attrs_mutator,
! (void *) &context);
if (newnode->resultRelation == appinfo->parent_relid)
{
! newnode->resultRelation = (appinfo->child_result > 0 ?
! appinfo->child_result :
! appinfo->child_relid);
! newnode->sourceRelation = appinfo->child_relid;
/* Fix tlist resnos too, if it's inherited UPDATE */
if (newnode->commandType == CMD_UPDATE)
newnode->targetList =
*************** adjust_appendrel_attrs(PlannerInfo *root
*** 1624,1629 ****
--- 1645,1693 ----
}
static Node *
+ fixup_var_on_rowsec_subquery(RangeTblEntry *rte, Var *var)
+ {
+ ListCell *cell;
+
+ Assert(rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_SECURITY);
+ /*
+ * In case when row-level security policy is applied on the referenced
+ * table, its RangeTblEntry (RTE_RELATION) is replaced with sub-query
+ * to filter out unprivileged rows of underlying relation.
+ * Even though reference to this sub-query should perform as if ones
+ * to real relations, system column has to be cared in special way
+ * due to the nature of sub-query.
+ * Target-entries that reference system columns should be added on
+ * rowlevelsec.c, so all we need to do here is looking up underlying
+ * target-list that can reference underlying system column, and fix-
+ * up varattno of the referencing Var node with resno of TargetEntry.
+ */
+ foreach (cell, rte->subquery->targetList)
+ {
+ TargetEntry *subtle = lfirst(cell);
+
+ if (IsA(subtle->expr, Var))
+ {
+ Var *subvar = (Var *) subtle->expr;
+ Var *newnode;
+
+ if (subvar->varattno == var->varattno)
+ {
+ newnode = copyObject(var);
+ newnode->varattno = subtle->resno;
+ return (Node *)newnode;
+ }
+ }
+ else
+ Assert(IsA(subtle->expr, Const));
+ }
+ elog(ERROR, "could not find artificial column of %d, relid %u",
+ var->varattno, var->varno);
+ return NULL;
+ }
+
+ static Node *
adjust_appendrel_attrs_mutator(Node *node,
adjust_appendrel_attrs_context *context)
{
*************** adjust_appendrel_attrs_mutator(Node *nod
*** 1638,1645 ****
if (var->varlevelsup == 0 &&
var->varno == appinfo->parent_relid)
{
! var->varno = appinfo->child_relid;
var->varnoold = appinfo->child_relid;
if (var->varattno > 0)
{
Node *newnode;
--- 1702,1713 ----
if (var->varlevelsup == 0 &&
var->varno == appinfo->parent_relid)
{
! var->varno = (context->in_returning &&
! appinfo->child_result > 0 ?
! appinfo->child_result :
! appinfo->child_relid);
var->varnoold = appinfo->child_relid;
+
if (var->varattno > 0)
{
Node *newnode;
*************** adjust_appendrel_attrs_mutator(Node *nod
*** 1664,1669 ****
--- 1732,1745 ----
*/
if (OidIsValid(appinfo->child_reltype))
{
+ Query *parse = context->root->parse;
+ RangeTblEntry *rte = rt_fetch(appinfo->child_relid,
+ parse->rtable);
+ if (!context->in_returning &&
+ rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_SECURITY)
+ var = (Var *)fixup_var_on_rowsec_subquery(rte, var);
+
Assert(var->vartype == appinfo->parent_reltype);
if (appinfo->parent_reltype != appinfo->child_reltype)
{
*************** adjust_appendrel_attrs_mutator(Node *nod
*** 1708,1714 ****
return (Node *) rowexpr;
}
}
! /* system attributes don't need any other translation */
}
return (Node *) var;
}
--- 1784,1801 ----
return (Node *) rowexpr;
}
}
! else
! {
! Query *parse = context->root->parse;
! RangeTblEntry *rte;
!
! rte = rt_fetch(appinfo->child_relid, parse->rtable);
!
! if (!context->in_returning &&
! rte->rtekind == RTE_SUBQUERY &&
! rte->subquery->querySource == QSRC_ROW_SECURITY)
! return fixup_var_on_rowsec_subquery(rte, var);
! }
}
return (Node *) var;
}
diff --git a/src/backend/optimizer/util/Makefile b/src/backend/optimizer/util/Makefile
new file mode 100644
index 3b2d16b..3f5cb19
*** a/src/backend/optimizer/util/Makefile
--- b/src/backend/optimizer/util/Makefile
*************** top_builddir = ../../../..
*** 13,18 ****
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
include $(top_srcdir)/src/backend/common.mk
--- 13,18 ----
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 rowsecurity.o
include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/optimizer/util/rowsecurity.c b/src/backend/optimizer/util/rowsecurity.c
new file mode 100644
index ...d686dfa
*** a/src/backend/optimizer/util/rowsecurity.c
--- b/src/backend/optimizer/util/rowsecurity.c
***************
*** 0 ****
--- 1,733 ----
+ /*
+ * optimizer/util/rowsecurity.c
+ * Routines to support row-security feature
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ */
+ #include "postgres.h"
+
+ #include "access/heapam.h"
+ #include "access/htup_details.h"
+ #include "access/sysattr.h"
+ #include "catalog/pg_class.h"
+ #include "catalog/pg_inherits_fn.h"
+ #include "catalog/pg_rowsecurity.h"
+ #include "catalog/pg_type.h"
+ #include "miscadmin.h"
+ #include "nodes/makefuncs.h"
+ #include "nodes/nodeFuncs.h"
+ #include "nodes/plannodes.h"
+ #include "optimizer/clauses.h"
+ #include "optimizer/prep.h"
+ #include "optimizer/rowsecurity.h"
+ #include "parser/parsetree.h"
+ #include "rewrite/rewriteHandler.h"
+ #include "utils/lsyscache.h"
+ #include "utils/rel.h"
+ #include "utils/syscache.h"
+ #include "tcop/utility.h"
+
+ /* flags to pull row-security policy */
+ #define RSEC_FLAG_HAS_SUBLINKS 0x0001
+
+ /* hook to allow extensions to apply their own security policy */
+ row_security_policy_hook_type row_security_policy_hook = NULL;
+
+ /*
+ * make_artificial_column
+ *
+ * It makes a target-entry node that references underlying column.
+ * Its tle->expr is usualy Var node, but may be Const for dummy NULL
+ * if the supplied attribute was already dropped.
+ */
+ static TargetEntry *
+ make_artificial_column(RangeTblEntry *subrte, AttrNumber attnum)
+ {
+ Expr *expr;
+ char *resname;
+
+ Assert(subrte->rtekind == RTE_RELATION && OidIsValid(subrte->relid));
+ if (attnum == InvalidAttrNumber)
+ {
+ expr = (Expr *) makeWholeRowVar(subrte, (Index) 1, 0, false);
+ resname = get_rel_name(subrte->relid);
+ }
+ else
+ {
+ HeapTuple tuple;
+ Form_pg_attribute attform;
+
+ tuple = SearchSysCache2(ATTNUM,
+ ObjectIdGetDatum(subrte->relid),
+ Int16GetDatum(attnum));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for attribute %d of relation %u",
+ attnum, subrte->relid);
+ attform = (Form_pg_attribute) GETSTRUCT(tuple);
+
+ if (attform->attisdropped)
+ {
+ char namebuf[NAMEDATALEN];
+
+ /* Insert NULL just for a placeholder of dropped column */
+ expr = (Expr *) makeConst(INT4OID,
+ -1,
+ InvalidOid,
+ sizeof(int32),
+ (Datum) 0,
+ true, /* isnull */
+ true); /* byval */
+ sprintf(namebuf, "dummy-%d", (int)attform->attnum);
+ resname = pstrdup(namebuf);
+ }
+ else
+ {
+ expr = (Expr *) makeVar((Index) 1,
+ attform->attnum,
+ attform->atttypid,
+ attform->atttypmod,
+ attform->attcollation,
+ 0);
+ resname = pstrdup(NameStr(attform->attname));
+ }
+ ReleaseSysCache(tuple);
+ }
+ return makeTargetEntry(expr, -1, resname, false);
+ }
+
+ /*
+ * lookup_artificial_column
+ *
+ * It looks-up resource number of the target-entry relevant to the given
+ * Var-node that references the row-security subquery. If required column
+ * is not in the subquery's target-list, this function also adds new one
+ * and returns its resource number.
+ */
+ static AttrNumber
+ lookup_artificial_column(PlannerInfo *root,
+ RangeTblEntry *rte, AttrNumber varattno)
+ {
+ Query *subqry;
+ RangeTblEntry *subrte;
+ TargetEntry *subtle;
+ ListCell *cell;
+
+ Assert(rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_SECURITY);
+
+ subqry = rte->subquery;
+ foreach (cell, subqry->targetList)
+ {
+ subtle = lfirst(cell);
+
+ /*
+ * If referenced artifical column is already constructed on the
+ * target-list of row-security subquery, nothing to do any more.
+ */
+ if (IsA(subtle->expr, Var))
+ {
+ Var *subvar = (Var *)subtle->expr;
+
+ Assert(subvar->varno == 1);
+ if (subvar->varattno == varattno)
+ return subtle->resno;
+ }
+ }
+
+ /*
+ * OK, we don't have an artifical column relevant to the required ones,
+ * so let's create a new artifical column on demand.
+ */
+ subrte = rt_fetch((Index) 1, subqry->rtable);
+ subtle = make_artificial_column(subrte, varattno);
+ subtle->resno = list_length(subqry->targetList) + 1;
+
+ subqry->targetList = lappend(subqry->targetList, subtle);
+ rte->eref->colnames = lappend(rte->eref->colnames,
+ makeString(pstrdup(subtle->resname)));
+ return subtle->resno;
+ }
+
+ /*
+ * fixup_varnode_walker
+ *
+ * It recursively fixes up references to the relation to be replaced by
+ * row-security sub-query, and adds artificial columns relevant to the
+ * underlying system columns or whole row-reference on demand.
+ */
+ typedef struct {
+ PlannerInfo *root;
+ int varlevelsup;
+ Index *vartrans;
+ } fixup_varnode_context;
+
+ static bool
+ fixup_varnode_walker(Node *node, fixup_varnode_context *context)
+ {
+ if (node == NULL)
+ return false;
+
+ if (IsA(node, Var))
+ {
+ Var *var = (Var *) node;
+ List *rtable = context->root->parse->rtable;
+ RangeTblEntry *rte;
+ ListCell *cell;
+
+ /*
+ * Ignore it, if Var node does not reference the Query currently
+ * we focues on.
+ */
+ if (var->varlevelsup != context->varlevelsup)
+ return false;
+
+ /*
+ * Var nodes that reference the relation being replaced by row-
+ * security sub-query has to be adjusted; to reference the sub-
+ * query, instead of the original relation.
+ */
+ if (context->vartrans[var->varno] != 0)
+ {
+ rte = rt_fetch(context->vartrans[var->varno], rtable);
+ if (rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_SECURITY)
+ {
+ var->varno = var->varnoold = context->vartrans[var->varno];
+ var->varattno = lookup_artificial_column(context->root,
+ rte, var->varattno);
+ }
+ }
+ else
+ {
+ rte = rt_fetch(var->varno, rtable);
+ if (!rte->inh)
+ return false;
+
+ foreach (cell, context->root->append_rel_list)
+ {
+ AppendRelInfo *appinfo = lfirst(cell);
+ RangeTblEntry *child_rte;
+
+ if (appinfo->parent_relid != var->varno)
+ continue;
+
+ if (var->varattno > InvalidAttrNumber)
+ continue;
+
+ child_rte = rt_fetch(appinfo->child_relid, rtable);
+ if (child_rte->rtekind == RTE_SUBQUERY &&
+ child_rte->subquery->querySource == QSRC_ROW_SECURITY)
+ (void) lookup_artificial_column(context->root,
+ child_rte,
+ var->varattno);
+ }
+ }
+ }
+ else if (IsA(node, RangeTblRef))
+ {
+ RangeTblRef *rtr = (RangeTblRef *) node;
+
+ if (context->varlevelsup == 0 &&
+ context->vartrans[rtr->rtindex] != 0)
+ rtr->rtindex = context->vartrans[rtr->rtindex];
+ }
+ else if (IsA(node, Query))
+ {
+ bool result;
+
+ context->varlevelsup++;
+ result = query_tree_walker((Query *) node,
+ fixup_varnode_walker,
+ (void *) context, 0);
+ context->varlevelsup--;
+
+ return result;
+ }
+ return expression_tree_walker(node,
+ fixup_varnode_walker,
+ (void *) context);
+ }
+
+ /*
+ * check_infinite_recursion
+ *
+ * It is a wrong row-security configuration, if we try to expand
+ * the relation inside of row-security subquery originated from
+ * same relation!
+ */
+ static void
+ check_infinite_recursion(PlannerInfo *root, Oid relid)
+ {
+ PlannerInfo *parent = root->parent_root;
+
+ if (parent && parent->parse->querySource == QSRC_ROW_SECURITY)
+ {
+ RangeTblEntry *rte = rt_fetch(1, parent->parse->rtable);
+
+ Assert(rte->rtekind == RTE_RELATION && OidIsValid(rte->relid));
+
+ if (relid == rte->relid)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("infinite recursion detected for relation \"%s\"",
+ get_rel_name(relid))));
+ check_infinite_recursion(parent, relid);
+ }
+ }
+
+ /*
+ * expand_rtentry_with_policy
+ *
+ * It extends a range-table entry of row-security sub-query with supplied
+ * security policy, and append it on the parse->rtable.
+ * This sub-query contains artificial columns that reference underlying
+ * regular columns (at least, references to system column or whole of
+ * table reference shall be added on demand), and simple scan on the
+ * target relation.
+ * Any Var nodes that referenced the relation pointed by rtindex shall
+ * be adjusted to reference this sub-query instead. walker
+ *
+ */
+ static Index
+ expand_rtentry_with_policy(PlannerInfo *root, Index rtindex,
+ Expr *qual, int flags)
+ {
+ Query *parse = root->parse;
+ RangeTblEntry *rte = rt_fetch(rtindex, parse->rtable);
+ Query *subqry;
+ RangeTblEntry *subrte;
+ RangeTblRef *subrtr;
+ TargetEntry *subtle;
+ RangeTblEntry *newrte;
+ HeapTuple tuple;
+ AttrNumber nattrs;
+ AttrNumber attnum;
+ List *targetList = NIL;
+ List *colNameList = NIL;
+ PlanRowMark *rowmark;
+
+ Assert(rte->rtekind == RTE_RELATION && !rte->inh);
+
+ /* check recursion to prevent infinite loop */
+ check_infinite_recursion(root, rte->relid);
+
+ /* Expand views inside SubLink node */
+ if (flags & RSEC_FLAG_HAS_SUBLINKS)
+ QueryRewriteExpr((Node *)qual, list_make1_oid(rte->relid));
+
+ /*
+ * Construction of sub-query
+ */
+ subqry = (Query *) makeNode(Query);
+ subqry->commandType = CMD_SELECT;
+ subqry->querySource = QSRC_ROW_SECURITY;
+
+ subrte = copyObject(rte);
+ subqry->rtable = list_make1(subrte);
+
+ subrtr = makeNode(RangeTblRef);
+ subrtr->rtindex = 1;
+ subqry->jointree = makeFromExpr(list_make1(subrtr), (Node *) qual);
+ if (flags & RSEC_FLAG_HAS_SUBLINKS)
+ subqry->hasSubLinks = true;
+
+ /*
+ * Construction of TargetEntries that reference underlying columns.
+ */
+ tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(rte->relid));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for relation %u", rte->relid);
+ nattrs = ((Form_pg_class) GETSTRUCT(tuple))->relnatts;
+ ReleaseSysCache(tuple);
+
+ for (attnum = 1; attnum <= nattrs; attnum++)
+ {
+ subtle = make_artificial_column(subrte, attnum);
+ subtle->resno = list_length(targetList) + 1;
+ Assert(subtle->resno == attnum);
+
+ targetList = lappend(targetList, subtle);
+ colNameList = lappend(colNameList,
+ makeString(pstrdup(subtle->resname)));
+ }
+ subqry->targetList = targetList;
+
+ /* Expand RengeTblEntry with this sub-query */
+ newrte = makeNode(RangeTblEntry);
+ newrte->rtekind = RTE_SUBQUERY;
+ newrte->subquery = subqry;
+ newrte->security_barrier = true;
+ newrte->rowsec_relid = rte->relid;
+ newrte->eref = makeAlias(get_rel_name(rte->relid), colNameList);
+
+ parse->rtable = lappend(parse->rtable, newrte);
+
+ /* Push-down rowmark, if needed */
+ rowmark = get_plan_rowmark(root->rowMarks, rtindex);
+ if (rowmark)
+ {
+ /*
+ * In case of inherited children, rti/prti of rowmark shall be
+ * fixed up later, on inheritance_planner().
+ */
+ if (rowmark->rti == rowmark->prti)
+ rowmark->rti = rowmark->prti = list_length(parse->rtable);
+ else
+ rowmark->rti = list_length(parse->rtable);
+
+ lookup_artificial_column(root, newrte, SelfItemPointerAttributeNumber);
+ lookup_artificial_column(root, newrte, TableOidAttributeNumber);
+ }
+
+ return list_length(parse->rtable);
+ }
+
+ /*
+ * pull_row_security_policy
+ *
+ * It pulls the configured row-security policy of both built-in and
+ * extensions. If any, it returns expression tree.
+ */
+ static Expr *
+ pull_row_security_policy(CmdType cmd, Relation relation, int *p_flags)
+ {
+ Expr *quals = NULL;
+ int flags = 0;
+
+ /*
+ * Pull the row-security policy configured with built-in features,
+ * if unprivileged users. Please note that superuser can bypass it.
+ */
+ if (relation->rsdesc && !superuser())
+ {
+ RowSecurityDesc *rsdesc = relation->rsdesc;
+
+ quals = copyObject(rsdesc->rsall.qual);
+ if (rsdesc->rsall.hassublinks)
+ flags |= RSEC_FLAG_HAS_SUBLINKS;
+ }
+
+ /*
+ * Also, ask extensions whether they want to apply their own
+ * row-security policy. If both built-in and extension has
+ * their own policy, it shall be merged.
+ */
+ if (row_security_policy_hook)
+ {
+ List *temp;
+
+ temp = (*row_security_policy_hook)(cmd, relation);
+ if (temp != NIL)
+ {
+ if ((flags & RSEC_FLAG_HAS_SUBLINKS) == 0 &&
+ contain_subplans((Node *) temp))
+ flags |= RSEC_FLAG_HAS_SUBLINKS;
+
+ if (quals != NULL)
+ temp = lappend(temp, quals);
+
+ if (list_length(temp) == 1)
+ quals = (Expr *)list_head(temp);
+ else if (list_length(temp) > 1)
+ quals = makeBoolExpr(AND_EXPR, temp, -1);
+ }
+ }
+ *p_flags = flags;
+ return quals;
+ }
+
+ /*
+ * copy_row_security_policy
+ *
+ * It construct a row-security subquery instead of raw COPY TO statement,
+ * if target relation has a row-level security policy
+ */
+ bool
+ copy_row_security_policy(CopyStmt *stmt, Relation rel, List *attnums)
+ {
+ Expr *quals;
+ int flags;
+ Query *parse;
+ RangeTblEntry *rte;
+ RangeTblRef *rtr;
+ TargetEntry *tle;
+ Var *var;
+ ListCell *cell;
+
+ if (stmt->is_from)
+ return false;
+
+ quals = pull_row_security_policy(CMD_SELECT, rel, &flags);
+ if (!quals)
+ return false;
+
+ parse = (Query *) makeNode(Query);
+ parse->commandType = CMD_SELECT;
+ parse->querySource = QSRC_ROW_SECURITY;
+
+ rte = makeNode(RangeTblEntry);
+ rte->rtekind = RTE_RELATION;
+ rte->relid = RelationGetRelid(rel);
+ rte->relkind = RelationGetForm(rel)->relkind;
+
+ foreach (cell, attnums)
+ {
+ HeapTuple tuple;
+ Form_pg_attribute attform;
+ AttrNumber attno = lfirst_int(cell);
+
+ tuple = SearchSysCache2(ATTNUM,
+ ObjectIdGetDatum(RelationGetRelid(rel)),
+ Int16GetDatum(attno));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for attribute %d of relation %s",
+ attno, RelationGetRelationName(rel));
+ attform = (Form_pg_attribute) GETSTRUCT(tuple);
+
+ var = makeVar((Index) 1,
+ attform->attnum,
+ attform->atttypid,
+ attform->atttypmod,
+ attform->attcollation,
+ 0);
+ tle = makeTargetEntry((Expr *) var,
+ list_length(parse->targetList) + 1,
+ pstrdup(NameStr(attform->attname)),
+ false);
+ parse->targetList = lappend(parse->targetList, tle);
+
+ ReleaseSysCache(tuple);
+
+ rte->selectedCols = bms_add_member(rte->selectedCols,
+ attno - FirstLowInvalidHeapAttributeNumber);
+ }
+ rte->inFromCl = true;
+ rte->requiredPerms = ACL_SELECT;
+
+ rtr = makeNode(RangeTblRef);
+ rtr->rtindex = 1;
+
+ parse->jointree = makeFromExpr(list_make1(rtr), (Node *) quals);
+ parse->rtable = list_make1(rte);
+ if (flags & RSEC_FLAG_HAS_SUBLINKS)
+ parse->hasSubLinks = true;
+
+ stmt->query = (Node *) parse;
+
+ return true;
+ }
+
+ /*
+ * apply_row_security_relation
+ *
+ * It applies row-security policy on a particular relation being specified.
+ * If this relation is top of the inheritance tree, it also checks inherited
+ * children.
+ */
+ static bool
+ apply_row_security_relation(PlannerInfo *root, Index *vartrans,
+ CmdType cmd, Index rtindex)
+ {
+ Query *parse = root->parse;
+ RangeTblEntry *rte = rt_fetch(rtindex, parse->rtable);
+ bool result = false;
+
+ if (!rte->inh)
+ {
+ Relation rel;
+ Expr *qual;
+ int flags = 0;
+
+ rel = heap_open(rte->relid, NoLock);
+ qual = pull_row_security_policy(cmd, rel, &flags);
+ if (qual)
+ {
+ vartrans[rtindex]
+ = expand_rtentry_with_policy(root, rtindex, qual, flags);
+ if (parse->resultRelation == rtindex)
+ parse->sourceRelation = vartrans[rtindex];
+ result = true;
+ }
+ heap_close(rel, NoLock);
+ }
+ else
+ {
+ ListCell *lc1, *lc2;
+
+ foreach (lc1, root->append_rel_list)
+ {
+ AppendRelInfo *apinfo = lfirst(lc1);
+
+ if (apinfo->parent_relid != rtindex)
+ continue;
+
+ if (apply_row_security_relation(root, vartrans, cmd,
+ apinfo->child_relid))
+ {
+ if (parse->resultRelation == rtindex)
+ apinfo->child_result = apinfo->child_relid;
+ apinfo->child_relid = vartrans[apinfo->child_relid];
+ foreach (lc2, apinfo->translated_vars)
+ {
+ Var *var = lfirst(lc2);
+
+ if (var)
+ var->varno = apinfo->child_relid;
+ }
+ result = true;
+ }
+ }
+ }
+ return result;
+ }
+
+ /*
+ * apply_row_security_recursive
+ *
+ * walker on join-tree
+ */
+ static bool
+ apply_row_security_recursive(PlannerInfo *root, Index *vartrans, Node *jtnode)
+ {
+ bool result = false;
+
+ if (jtnode == NULL)
+ return false;
+ if (IsA(jtnode, RangeTblRef))
+ {
+ Index rtindex = ((RangeTblRef *) jtnode)->rtindex;
+ Query *parse = root->parse;
+ RangeTblEntry *rte = rt_fetch(rtindex, parse->rtable);
+ CmdType cmd;
+
+ /* Only relation can have row-security policy */
+ if (rte->rtekind != RTE_RELATION)
+ return false;
+
+ /*
+ * Prevents infinite recursion. Please note that rtindex == 1
+ * of the row-security subquery is a relation being already
+ * processed on the upper level.
+ */
+ if (parse->querySource == QSRC_ROW_SECURITY && rtindex == 1)
+ return false;
+
+ /* Is it a result relation of UPDATE or DELETE command? */
+ if (parse->resultRelation == rtindex)
+ cmd = parse->commandType;
+ else
+ cmd = CMD_SELECT;
+
+ /* Try to apply row-security policy, if configured */
+ result = apply_row_security_relation(root, vartrans, cmd, rtindex);
+ }
+ else if (IsA(jtnode, FromExpr))
+ {
+ FromExpr *f = (FromExpr *) jtnode;
+ ListCell *l;
+
+ foreach (l, f->fromlist)
+ {
+ if (apply_row_security_recursive(root, vartrans, lfirst(l)))
+ result = true;
+ }
+ }
+ else if (IsA(jtnode, JoinExpr))
+ {
+ JoinExpr *j = (JoinExpr *) jtnode;
+
+ if (apply_row_security_recursive(root, vartrans, j->larg))
+ result = true;
+ if (apply_row_security_recursive(root, vartrans, j->rarg))
+ result = true;
+ }
+ else
+ elog(ERROR, "unexpected node type: %d", (int) nodeTag(jtnode));
+
+ return result;
+ }
+
+ /*
+ * apply_row_security_policy
+ *
+ * Entrypoint to apply configured row-security policy of the relation.
+ *
+ * In case when the supplied query references relations with row-security
+ * policy, its RangeTblEntry shall be replaced by a row-security subquery
+ * that has simple scan on the referenced table with policy qualifiers.
+ * Of course, security-barrier shall be set on the subquery to prevent
+ * unexpected push-down of functions without leakproof flag.
+ *
+ * For example, when table t1 has a security policy "(x % 2 = 0)", the
+ * following query:
+ * SELECT * FROM t1 WHERE f_leak(y)
+ * performs as if
+ * SELECT * FROM (
+ * SELECT x, y FROM t1 WHERE (x % 2 = 0)
+ * ) AS t1 WHERE f_leak(y)
+ * would be given. Because the sub-query has security barrier flag,
+ * configured security policy qualifier is always executed prior to
+ * user given functions.
+ */
+ void
+ apply_row_security_policy(PlannerInfo *root)
+ {
+ Query *parse = root->parse;
+ Oid curr_userid;
+ int curr_seccxt;
+ Index *vartrans;
+
+ /*
+ * Mode checks. In case when SECURITY_ROW_LEVEL_DISABLED is set,
+ * no row-level security policy should be applied regardless
+ * whether it is built-in or extension.
+ */
+ GetUserIdAndSecContext(&curr_userid, &curr_seccxt);
+ if (curr_seccxt & SECURITY_ROW_LEVEL_DISABLED)
+ return;
+
+ vartrans = palloc0(sizeof(Index) * (list_length(parse->rtable) + 1));
+ if (apply_row_security_recursive(root, vartrans, (Node *)parse->jointree))
+ {
+ PlannerGlobal *glob = root->glob;
+ PlanInvalItem *pi_item;
+ fixup_varnode_context context;
+
+ /*
+ * Constructed Plan with row-level security policy depends on
+ * properties of current user (database superuser can bypass
+ * configured row-security policy!), thus, it has to be
+ * invalidated when its assumption was changed.
+ */
+ if (!OidIsValid(glob->planUserId))
+ {
+ /* Plan invalidation on session user-id */
+ glob->planUserId = GetUserId();
+
+ /* Plan invalidation on catalog updates of pg_authid */
+ pi_item = makeNode(PlanInvalItem);
+ pi_item->cacheId = AUTHOID;
+ pi_item->hashValue =
+ GetSysCacheHashValue1(AUTHOID,
+ ObjectIdGetDatum(glob->planUserId));
+ glob->invalItems = lappend(glob->invalItems, pi_item);
+ }
+ else
+ Assert(glob->planUserId == GetUserId());
+
+ /*
+ * Var-nodes that referenced RangeTblEntry to be replaced by
+ * row-security sub-query have to be adjusted for appropriate
+ * reference to the underlying artificial column of the relation.
+ */
+ context.root = root;
+ context.varlevelsup = 0;
+ context.vartrans = vartrans;
+ query_tree_walker(parse,
+ fixup_varnode_walker,
+ (void *) &context,
+ QTW_IGNORE_RETURNING);
+ }
+ pfree(vartrans);
+ }
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
new file mode 100644
index d8d2bdf..f685e43
*** a/src/backend/parser/gram.y
--- b/src/backend/parser/gram.y
*************** static Node *makeRecursiveViewSelect(cha
*** 256,261 ****
--- 256,262 ----
%type <node> alter_table_cmd alter_type_cmd opt_collate_clause
%type <list> alter_table_cmds alter_type_cmds
+ %type <str> row_security_cmd
%type <dbehavior> opt_drop_behavior
*************** alter_table_cmd:
*** 2174,2179 ****
--- 2175,2198 ----
n->def = (Node *)$2;
$$ = (Node *)n;
}
+ /* ALTER TABLE <name> SET ROW SECURITY FOR <cmd> TO (<expr>) */
+ | SET ROW SECURITY FOR row_security_cmd TO '(' a_expr ')'
+ {
+ AlterTableCmd *n = makeNode(AlterTableCmd);
+ n->subtype = AT_SetRowSecurity;
+ n->name = $5;
+ n->def = (Node *) $8;
+ $$ = (Node *)n;
+ }
+ /* ALTER TABLE <name> RESET ROW SECURITY FOR <cmd> */
+ | RESET ROW SECURITY FOR row_security_cmd
+ {
+ AlterTableCmd *n = makeNode(AlterTableCmd);
+ n->subtype = AT_ResetRowSecurity;
+ n->name = $5;
+ n->def = NULL;
+ $$ = (Node *)n;
+ }
| alter_generic_options
{
AlterTableCmd *n = makeNode(AlterTableCmd);
*************** reloption_elem:
*** 2245,2250 ****
--- 2264,2275 ----
}
;
+ row_security_cmd: ALL { $$ = "all"; }
+ | SELECT { $$ = "select"; }
+ | INSERT { $$ = "insert"; }
+ | UPDATE { $$ = "update"; }
+ | DELETE_P { $$ = "delete"; }
+ ;
/*****************************************************************************
*
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
new file mode 100644
index 4e4e1cd..d9049e0
*** a/src/backend/parser/parse_agg.c
--- b/src/backend/parser/parse_agg.c
*************** transformAggregateCall(ParseState *pstat
*** 272,277 ****
--- 272,280 ----
case EXPR_KIND_TRIGGER_WHEN:
err = _("aggregate functions are not allowed in trigger WHEN conditions");
break;
+ case EXPR_KIND_ROW_SECURITY:
+ err = _("aggregate functions are not allowed in row-security policy");
+ break;
/*
* There is intentionally no default: case here, so that the
*************** transformWindowFuncCall(ParseState *psta
*** 547,552 ****
--- 550,558 ----
case EXPR_KIND_TRIGGER_WHEN:
err = _("window functions are not allowed in trigger WHEN conditions");
break;
+ case EXPR_KIND_ROW_SECURITY:
+ err = _("window functions are not allowed in row-security policy");
+ break;
/*
* There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
new file mode 100644
index 68b711d..fb09c8c
*** a/src/backend/parser/parse_expr.c
--- b/src/backend/parser/parse_expr.c
*************** transformSubLink(ParseState *pstate, Sub
*** 1459,1464 ****
--- 1459,1465 ----
case EXPR_KIND_OFFSET:
case EXPR_KIND_RETURNING:
case EXPR_KIND_VALUES:
+ case EXPR_KIND_ROW_SECURITY:
/* okay */
break;
case EXPR_KIND_CHECK_CONSTRAINT:
*************** ParseExprKindName(ParseExprKind exprKind
*** 2640,2645 ****
--- 2641,2648 ----
return "EXECUTE";
case EXPR_KIND_TRIGGER_WHEN:
return "WHEN";
+ case EXPR_KIND_ROW_SECURITY:
+ return "ROW SECURITY";
/*
* There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c
new file mode 100644
index a9254c8..768d531
*** a/src/backend/parser/parse_relation.c
--- b/src/backend/parser/parse_relation.c
*************** expandRelAttrs(ParseState *pstate, Range
*** 2058,2063 ****
--- 2058,2082 ----
}
/*
+ * getrelid
+ *
+ * Get OID of the relation corresponding to the given range index.
+ * Note that InvalidOid will be returned if the RTE is for neither
+ * relation nor sub-query originated from a relation
+ */
+ Oid
+ getrelid(Index rangeindex, List *rangetable)
+ {
+ RangeTblEntry *rte = rt_fetch(rangeindex, rangetable);
+
+ if (rte->rtekind == RTE_RELATION)
+ return rte->relid;
+ if (rte->rtekind == RTE_SUBQUERY)
+ return rte->rowsec_relid;
+ return InvalidOid;
+ }
+
+ /*
* get_rte_attribute_name
* Get an attribute name from a RangeTblEntry
*
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
new file mode 100644
index 7f527bd..2d675ed
*** a/src/backend/rewrite/rewriteHandler.c
--- b/src/backend/rewrite/rewriteHandler.c
*************** QueryRewrite(Query *parsetree)
*** 3099,3101 ****
--- 3099,3117 ----
return results;
}
+
+ /*
+ * QueryRewriteExpr
+ *
+ * This routine provides an entry point of query rewriter towards
+ * a certain expression tree with SubLink node; being added after
+ * the top level query rewrite.
+ * It primarily intends to expand views appeared in the qualifiers
+ * appended with row-level security which needs to modify query
+ * tree at head of the planner stage.
+ */
+ void
+ QueryRewriteExpr(Node *node, List *activeRIRs)
+ {
+ fireRIRonSubLink(node, activeRIRs);
+ }
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
new file mode 100644
index 65edc1f..b36da97
*** a/src/backend/utils/adt/ri_triggers.c
--- b/src/backend/utils/adt/ri_triggers.c
*************** ri_PerformCheck(const RI_ConstraintInfo
*** 3008,3013 ****
--- 3008,3014 ----
int spi_result;
Oid save_userid;
int save_sec_context;
+ int temp_sec_context;
Datum vals[RI_MAX_NUMKEYS * 2];
char nulls[RI_MAX_NUMKEYS * 2];
*************** ri_PerformCheck(const RI_ConstraintInfo
*** 3087,3094 ****
/* Switch to proper UID to perform check as */
GetUserIdAndSecContext(&save_userid, &save_sec_context);
SetUserIdAndSecContext(RelationGetForm(query_rel)->relowner,
! save_sec_context | SECURITY_LOCAL_USERID_CHANGE);
/* Finally we can run the query. */
spi_result = SPI_execute_snapshot(qplan,
--- 3088,3105 ----
/* Switch to proper UID to perform check as */
GetUserIdAndSecContext(&save_userid, &save_sec_context);
+
+ /*
+ * Row-level security should be disabled in case when foreign-key
+ * relation is queried to check existence of tuples that references
+ * the primary-key being modified.
+ */
+ temp_sec_context = save_sec_context | SECURITY_LOCAL_USERID_CHANGE;
+ if (source_is_pk)
+ temp_sec_context |= SECURITY_ROW_LEVEL_DISABLED;
+
SetUserIdAndSecContext(RelationGetForm(query_rel)->relowner,
! temp_sec_context);
/* Finally we can run the query. */
spi_result = SPI_execute_snapshot(qplan,
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
new file mode 100644
index 26cae97..d05cde7
*** a/src/backend/utils/cache/plancache.c
--- b/src/backend/utils/cache/plancache.c
***************
*** 53,58 ****
--- 53,59 ----
#include "catalog/namespace.h"
#include "executor/executor.h"
#include "executor/spi.h"
+ #include "miscadmin.h"
#include "nodes/nodeFuncs.h"
#include "optimizer/planmain.h"
#include "optimizer/prep.h"
*************** CheckCachedPlan(CachedPlanSource *planso
*** 794,799 ****
--- 795,810 ----
AcquireExecutorLocks(plan->stmt_list, true);
/*
+ * If plan was constructed with assumption of a particular user-id,
+ * and it is different from the current one, the cached-plan shall
+ * be invalidated to construct suitable query plan.
+ */
+ if (plan->is_valid &&
+ OidIsValid(plan->planUserId) &&
+ plan->planUserId == GetUserId())
+ plan->is_valid = false;
+
+ /*
* If plan was transient, check to see if TransactionXmin has
* advanced, and if so invalidate it.
*/
*************** BuildCachedPlan(CachedPlanSource *planso
*** 846,851 ****
--- 857,864 ----
{
CachedPlan *plan;
List *plist;
+ ListCell *cell;
+ Oid planUserId = InvalidOid;
bool snapshot_set;
bool spi_pushed;
MemoryContext plan_context;
*************** BuildCachedPlan(CachedPlanSource *planso
*** 913,918 ****
--- 926,949 ----
PopActiveSnapshot();
/*
+ * Check whether the generated plan assumes a particular user-id, or not.
+ * In case when a valid user-id is recorded on PlannedStmt->planUserId,
+ * it should be kept and used to validation check of the cached plan
+ * under the "current" user-id.
+ */
+ foreach (cell, plist)
+ {
+ PlannedStmt *pstmt = lfirst(cell);
+
+ if (IsA(pstmt, PlannedStmt) && OidIsValid(pstmt->planUserId))
+ {
+ Assert(!OidIsValid(planUserId) || planUserId == pstmt->planUserId);
+
+ planUserId = pstmt->planUserId;
+ }
+ }
+
+ /*
* Normally we make a dedicated memory context for the CachedPlan and its
* subsidiary data. (It's probably not going to be large, but just in
* case, use the default maxsize parameter. It's transient for the
*************** BuildCachedPlan(CachedPlanSource *planso
*** 955,960 ****
--- 986,992 ----
plan->is_oneshot = plansource->is_oneshot;
plan->is_saved = false;
plan->is_valid = true;
+ plan->planUserId = planUserId;
/* assign generation number to new plan */
plan->generation = ++(plansource->generation);
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
new file mode 100644
index 66fb63b..828c10e
*** a/src/backend/utils/cache/relcache.c
--- b/src/backend/utils/cache/relcache.c
***************
*** 50,55 ****
--- 50,56 ----
#include "catalog/pg_opclass.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_rewrite.h"
+ #include "catalog/pg_rowsecurity.h"
#include "catalog/pg_tablespace.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
*************** RelationBuildDesc(Oid targetRelId, bool
*** 935,940 ****
--- 936,946 ----
else
relation->trigdesc = NULL;
+ if (relation->rd_rel->relhasrowsecurity)
+ RelationBuildRowSecurity(relation);
+ else
+ relation->rsdesc = NULL;
+
/*
* if it's an index, initialize index-related information
*/
*************** RelationDestroyRelation(Relation relatio
*** 1842,1847 ****
--- 1848,1855 ----
MemoryContextDelete(relation->rd_indexcxt);
if (relation->rd_rulescxt)
MemoryContextDelete(relation->rd_rulescxt);
+ if (relation->rsdesc)
+ MemoryContextDelete(relation->rsdesc->rscxt);
if (relation->rd_fdwroutine)
pfree(relation->rd_fdwroutine);
pfree(relation);
*************** RelationCacheInitializePhase3(void)
*** 3161,3167 ****
relation->rd_rel->relhastriggers = false;
restart = true;
}
!
/* Release hold on the relation */
RelationDecrementReferenceCount(relation);
--- 3169,3181 ----
relation->rd_rel->relhastriggers = false;
restart = true;
}
! if (relation->rd_rel->relhasrowsecurity && relation->rsdesc == NULL)
! {
! RelationBuildRowSecurity(relation);
! if (relation->rsdesc == NULL)
! relation->rd_rel->relhasrowsecurity = false;
! restart = true;
! }
/* Release hold on the relation */
RelationDecrementReferenceCount(relation);
*************** load_relcache_init_file(bool shared)
*** 4407,4412 ****
--- 4421,4427 ----
rel->rd_rules = NULL;
rel->rd_rulescxt = NULL;
rel->trigdesc = NULL;
+ rel->rsdesc = NULL;
rel->rd_indexprs = NIL;
rel->rd_indpred = NIL;
rel->rd_exclops = NULL;
diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c
new file mode 100644
index 247ad92..9603ef4
*** a/src/bin/pg_dump/common.c
--- b/src/bin/pg_dump/common.c
*************** getSchemaData(Archive *fout, int *numTab
*** 244,249 ****
--- 244,253 ----
write_msg(NULL, "reading rewrite rules\n");
getRules(fout, &numRules);
+ if (g_verbose)
+ write_msg(NULL, "reading row-security policies\n");
+ getRowSecurity(fout, tblinfo, numTables);
+
*numTablesPtr = numTables;
return tblinfo;
}
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
new file mode 100644
index cd7669b..e9ca251
*** a/src/bin/pg_dump/pg_backup_archiver.c
--- b/src/bin/pg_dump/pg_backup_archiver.c
*************** _printTocEntry(ArchiveHandle *AH, TocEnt
*** 3148,3153 ****
--- 3148,3154 ----
strcmp(te->desc, "INDEX") == 0 ||
strcmp(te->desc, "RULE") == 0 ||
strcmp(te->desc, "TRIGGER") == 0 ||
+ strcmp(te->desc, "ROW SECURITY") == 0 ||
strcmp(te->desc, "USER MAPPING") == 0)
{
/* these object types don't have separate owners */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
new file mode 100644
index 8beb5d1..84f2519
*** a/src/bin/pg_dump/pg_dump.c
--- b/src/bin/pg_dump/pg_dump.c
*************** static char *myFormatType(const char *ty
*** 249,254 ****
--- 249,255 ----
static void getBlobs(Archive *fout);
static void dumpBlob(Archive *fout, BlobInfo *binfo);
static int dumpBlobs(Archive *fout, void *arg);
+ static void dumpRowSecurity(Archive *fout, RowSecurityInfo *rsinfo);
static void dumpDatabase(Archive *AH);
static void dumpEncoding(Archive *AH);
static void dumpStdStrings(Archive *AH);
*************** dumpBlobs(Archive *fout, void *arg)
*** 2686,2691 ****
--- 2687,2820 ----
return 1;
}
+ /*
+ * getRowSecurity
+ * get information about every row-security policy on a dumpable table
+ */
+ void
+ getRowSecurity(Archive *fout, TableInfo tblinfo[], int numTables)
+ {
+ PQExpBuffer query = createPQExpBuffer();
+ PGresult *res;
+ RowSecurityInfo *rsinfo;
+ int i_oid;
+ int i_tableoid;
+ int i_rseccmd;
+ int i_rsecqual;
+ int i, j, ntups;
+
+ /* row-security is not supported prior to v9.4 */
+ if (fout->remoteVersion < 90400)
+ return;
+
+ for (i=0; i < numTables; i++)
+ {
+ TableInfo *tbinfo = &tblinfo[i];
+
+ if (!tbinfo->hasrowsec || !tbinfo->dobj.dump)
+ continue;
+
+ if (g_verbose)
+ write_msg(NULL, "reading row-security policy for table \"%s\"\n",
+ tbinfo->dobj.name);
+
+ /*
+ * select table schema to ensure regproc name is qualified if needed
+ */
+ selectSourceSchema(fout, tbinfo->dobj.namespace->dobj.name);
+
+ resetPQExpBuffer(query);
+
+ appendPQExpBuffer(query,
+ "SELECT oid, tableoid, s.rseccmd, "
+ "pg_get_expr(s.rsecqual, s.rsecrelid) AS rsecqual "
+ "FROM pg_catalog.pg_rowsecurity s "
+ "WHERE rsecrelid = '%u'",
+ tbinfo->dobj.catId.oid);
+ res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+
+ ntups = PQntuples(res);
+
+ i_oid = PQfnumber(res, "oid");
+ i_tableoid = PQfnumber(res, "tableoid");
+ i_rseccmd = PQfnumber(res, "rseccmd");
+ i_rsecqual = PQfnumber(res, "rsecqual");
+
+ rsinfo = pg_malloc(ntups * sizeof(RowSecurityInfo));
+ for (j=0; j < ntups; j++)
+ {
+ char namebuf[NAMEDATALEN + 1];
+
+ rsinfo[j].dobj.objType = DO_ROW_SECURITY;
+ rsinfo[j].dobj.catId.tableoid =
+ atooid(PQgetvalue(res, j, i_tableoid));
+ rsinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
+ AssignDumpId(&rsinfo[j].dobj);
+ snprintf(namebuf, sizeof(namebuf), "row-security of %s",
+ tbinfo->rolname);
+ rsinfo[j].dobj.name = namebuf;
+ rsinfo[j].dobj.namespace = tbinfo->dobj.namespace;
+ rsinfo[j].rstable = tbinfo;
+ rsinfo[j].rseccmd = pg_strdup(PQgetvalue(res, j, i_rseccmd));
+ rsinfo[j].rsecqual = pg_strdup(PQgetvalue(res, j, i_rsecqual));
+ }
+ PQclear(res);
+ }
+ destroyPQExpBuffer(query);
+ }
+
+ /*
+ * dumpRowSecurity
+ * dump the definition of the given row-security policy
+ */
+ static void
+ dumpRowSecurity(Archive *fout, RowSecurityInfo *rsinfo)
+ {
+ TableInfo *tbinfo = rsinfo->rstable;
+ PQExpBuffer query;
+ PQExpBuffer delqry;
+ const char *cmd;
+
+ if (dataOnly || !tbinfo->hasrowsec)
+ return;
+
+ query = createPQExpBuffer();
+ delqry = createPQExpBuffer();
+ appendPQExpBuffer(query, "ALTER TABLE %s SET ROW SECURITY ",
+ fmtId(tbinfo->dobj.name));
+ appendPQExpBuffer(delqry, "ALTER TABLE %s RESET ROW SECURITY ",
+ fmtId(tbinfo->dobj.name));
+ if (strcmp(rsinfo->rseccmd, "a") == 0)
+ cmd = "ALL";
+ else if (strcmp(rsinfo->rseccmd, "s") == 0)
+ cmd = "SELECT";
+ else if (strcmp(rsinfo->rseccmd, "i") == 0)
+ cmd = "INSERT";
+ else if (strcmp(rsinfo->rseccmd, "u") == 0)
+ cmd = "UPDATE";
+ else if (strcmp(rsinfo->rseccmd, "d") == 0)
+ cmd = "DELETE";
+ else
+ {
+ write_msg(NULL, "unexpected command type: '%s'\n", rsinfo->rseccmd);
+ exit_nicely(1);
+ }
+ appendPQExpBuffer(query, "FOR %s TO %s;\n", cmd, rsinfo->rsecqual);
+ appendPQExpBuffer(delqry, "FOR %s;\n", cmd);
+
+ ArchiveEntry(fout, rsinfo->dobj.catId, rsinfo->dobj.dumpId,
+ rsinfo->dobj.name,
+ rsinfo->dobj.namespace->dobj.name,
+ NULL,
+ tbinfo->rolname, false,
+ "ROW SECURITY", SECTION_POST_DATA,
+ query->data, delqry->data, NULL,
+ NULL, 0,
+ NULL, NULL);
+
+ destroyPQExpBuffer(query);
+ }
+
static void
binary_upgrade_set_type_oids_by_type_oid(Archive *fout,
PQExpBuffer upgrade_buffer,
*************** getTables(Archive *fout, int *numTables)
*** 4214,4219 ****
--- 4343,4349 ----
int i_relhastriggers;
int i_relhasindex;
int i_relhasrules;
+ int i_relhasrowsec;
int i_relhasoids;
int i_relfrozenxid;
int i_toastoid;
*************** getTables(Archive *fout, int *numTables)
*** 4252,4258 ****
* we cannot correctly identify inherited columns, owned sequences, etc.
*/
! if (fout->remoteVersion >= 90300)
{
/*
* Left join to pick up dependency info linking sequences to their
--- 4382,4388 ----
* we cannot correctly identify inherited columns, owned sequences, etc.
*/
! if (fout->remoteVersion >= 90400)
{
/*
* Left join to pick up dependency info linking sequences to their
*************** getTables(Archive *fout, int *numTables)
*** 4264,4269 ****
--- 4394,4438 ----
"(%s c.relowner) AS rolname, "
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
+ "c.relhasrowsecurity, "
+ "c.relfrozenxid, tc.oid AS toid, "
+ "tc.relfrozenxid AS tfrozenxid, "
+ "c.relpersistence, c.relispopulated, "
+ "c.relpages, "
+ "CASE WHEN c.reloftype <> 0 THEN c.reloftype::pg_catalog.regtype ELSE NULL END AS reloftype, "
+ "d.refobjid AS owning_tab, "
+ "d.refobjsubid AS owning_col, "
+ "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
+ "array_to_string(c.reloptions, ', ') AS reloptions, "
+ "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions "
+ "FROM pg_class c "
+ "LEFT JOIN pg_depend d ON "
+ "(c.relkind = '%c' AND "
+ "d.classid = c.tableoid AND d.objid = c.oid AND "
+ "d.objsubid = 0 AND "
+ "d.refclassid = c.tableoid AND d.deptype = 'a') "
+ "LEFT JOIN pg_class tc ON (c.reltoastrelid = tc.oid) "
+ "WHERE c.relkind in ('%c', '%c', '%c', '%c', '%c', '%c') "
+ "ORDER BY c.oid",
+ username_subquery,
+ RELKIND_SEQUENCE,
+ RELKIND_RELATION, RELKIND_SEQUENCE,
+ RELKIND_VIEW, RELKIND_COMPOSITE_TYPE,
+ RELKIND_MATVIEW, RELKIND_FOREIGN_TABLE);
+ }
+ else if (fout->remoteVersion >= 90300)
+ {
+ /*
+ * Left join to pick up dependency info linking sequences to their
+ * owning column, if any (note this dependency is AUTO as of 8.2)
+ */
+ appendPQExpBuffer(query,
+ "SELECT c.tableoid, c.oid, c.relname, "
+ "c.relacl, c.relkind, c.relnamespace, "
+ "(%s c.relowner) AS rolname, "
+ "c.relchecks, c.relhastriggers, "
+ "c.relhasindex, c.relhasrules, c.relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"c.relfrozenxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"c.relpersistence, c.relispopulated, "
*************** getTables(Archive *fout, int *numTables)
*** 4303,4308 ****
--- 4472,4478 ----
"(%s c.relowner) AS rolname, "
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"c.relfrozenxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"c.relpersistence, 't' as relispopulated, "
*************** getTables(Archive *fout, int *numTables)
*** 4340,4345 ****
--- 4510,4516 ----
"(%s c.relowner) AS rolname, "
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"c.relfrozenxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"'p' AS relpersistence, 't' as relispopulated, "
*************** getTables(Archive *fout, int *numTables)
*** 4376,4381 ****
--- 4547,4553 ----
"(%s c.relowner) AS rolname, "
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"c.relfrozenxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"'p' AS relpersistence, 't' as relispopulated, "
*************** getTables(Archive *fout, int *numTables)
*** 4412,4417 ****
--- 4584,4590 ----
"(%s c.relowner) AS rolname, "
"c.relchecks, (c.reltriggers <> 0) AS relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"c.relfrozenxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"'p' AS relpersistence, 't' as relispopulated, "
*************** getTables(Archive *fout, int *numTables)
*** 4448,4453 ****
--- 4621,4627 ----
"(%s relowner) AS rolname, "
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"0 AS relfrozenxid, "
"0 AS toid, "
"0 AS tfrozenxid, "
*************** getTables(Archive *fout, int *numTables)
*** 4484,4489 ****
--- 4658,4664 ----
"(%s relowner) AS rolname, "
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"0 AS relfrozenxid, "
"0 AS toid, "
"0 AS tfrozenxid, "
*************** getTables(Archive *fout, int *numTables)
*** 4516,4521 ****
--- 4691,4697 ----
"(%s relowner) AS rolname, "
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"0 AS relfrozenxid, "
"0 AS toid, "
"0 AS tfrozenxid, "
*************** getTables(Archive *fout, int *numTables)
*** 4543,4548 ****
--- 4719,4725 ----
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, "
"'t'::bool AS relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"0 AS relfrozenxid, "
"0 AS toid, "
"0 AS tfrozenxid, "
*************** getTables(Archive *fout, int *numTables)
*** 4580,4585 ****
--- 4757,4763 ----
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, "
"'t'::bool AS relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"0 as relfrozenxid, "
"0 AS toid, "
"0 AS tfrozenxid, "
*************** getTables(Archive *fout, int *numTables)
*** 4627,4632 ****
--- 4805,4811 ----
i_relhastriggers = PQfnumber(res, "relhastriggers");
i_relhasindex = PQfnumber(res, "relhasindex");
i_relhasrules = PQfnumber(res, "relhasrules");
+ i_relhasrowsec = PQfnumber(res, "relhasrowsecurity");
i_relhasoids = PQfnumber(res, "relhasoids");
i_relfrozenxid = PQfnumber(res, "relfrozenxid");
i_toastoid = PQfnumber(res, "toid");
*************** getTables(Archive *fout, int *numTables)
*** 4675,4680 ****
--- 4854,4860 ----
tblinfo[i].hasindex = (strcmp(PQgetvalue(res, i, i_relhasindex), "t") == 0);
tblinfo[i].hasrules = (strcmp(PQgetvalue(res, i, i_relhasrules), "t") == 0);
tblinfo[i].hastriggers = (strcmp(PQgetvalue(res, i, i_relhastriggers), "t") == 0);
+ tblinfo[i].hasrowsec = (strcmp(PQgetvalue(res, i, i_relhasrowsec), "t") == 0);
tblinfo[i].hasoids = (strcmp(PQgetvalue(res, i, i_relhasoids), "t") == 0);
tblinfo[i].relispopulated = (strcmp(PQgetvalue(res, i, i_relispopulated), "t") == 0);
tblinfo[i].relpages = atoi(PQgetvalue(res, i, i_relpages));
*************** dumpDumpableObject(Archive *fout, Dumpab
*** 7758,7763 ****
--- 7938,7946 ----
NULL, 0,
dumpBlobs, NULL);
break;
+ case DO_ROW_SECURITY:
+ dumpRowSecurity(fout, (RowSecurityInfo *) dobj);
+ break;
case DO_PRE_DATA_BOUNDARY:
case DO_POST_DATA_BOUNDARY:
/* never dumped, nothing to do */
*************** addBoundaryDependencies(DumpableObject *
*** 14936,14941 ****
--- 15119,15125 ----
case DO_TRIGGER:
case DO_EVENT_TRIGGER:
case DO_DEFAULT_ACL:
+ case DO_ROW_SECURITY:
/* Post-data objects: must come after the post-data boundary */
addObjectDependency(dobj, postDataBound->dumpId);
break;
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
new file mode 100644
index 2c5971c..4dc00db
*** a/src/bin/pg_dump/pg_dump.h
--- b/src/bin/pg_dump/pg_dump.h
*************** typedef enum
*** 111,117 ****
DO_PRE_DATA_BOUNDARY,
DO_POST_DATA_BOUNDARY,
DO_EVENT_TRIGGER,
! DO_REFRESH_MATVIEW
} DumpableObjectType;
typedef struct _dumpableObject
--- 111,118 ----
DO_PRE_DATA_BOUNDARY,
DO_POST_DATA_BOUNDARY,
DO_EVENT_TRIGGER,
! DO_REFRESH_MATVIEW,
! DO_ROW_SECURITY,
} DumpableObjectType;
typedef struct _dumpableObject
*************** typedef struct _tableInfo
*** 244,249 ****
--- 245,251 ----
bool hasindex; /* does it have any indexes? */
bool hasrules; /* does it have any rules? */
bool hastriggers; /* does it have any triggers? */
+ bool hasrowsec; /* does it have any row-security policy? */
bool hasoids; /* does it have OIDs? */
uint32 frozenxid; /* for restore frozen xid */
Oid toast_oid; /* for restore toast frozen xid */
*************** typedef struct _blobInfo
*** 482,487 ****
--- 484,497 ----
char *blobacl;
} BlobInfo;
+ typedef struct _rowSecurityInfo
+ {
+ DumpableObject dobj;
+ TableInfo *rstable;
+ char *rseccmd;
+ char *rsecqual;
+ } RowSecurityInfo;
+
/* global decls */
extern bool force_quotes; /* double-quotes for identifiers flag */
extern bool g_verbose; /* verbose flag */
*************** extern DefaultACLInfo *getDefaultACLs(Ar
*** 573,577 ****
--- 583,588 ----
extern void getExtensionMembership(Archive *fout, ExtensionInfo extinfo[],
int numExtensions);
extern EventTriggerInfo *getEventTriggers(Archive *fout, int *numEventTriggers);
+ extern void getRowSecurity(Archive *fout, TableInfo tblinfo[], int numTables);
#endif /* PG_DUMP_H */
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
new file mode 100644
index 141e713..5c5777c
*** a/src/bin/pg_dump/pg_dump_sort.c
--- b/src/bin/pg_dump/pg_dump_sort.c
*************** describeDumpableObject(DumpableObject *o
*** 1342,1347 ****
--- 1342,1352 ----
"BLOB DATA (ID %d)",
obj->dumpId);
return;
+ case DO_ROW_SECURITY:
+ snprintf(buf, bufsize,
+ "ROW-SECURITY POLICY (ID %d OID %u)",
+ obj->dumpId, obj->catId.oid);
+ return;
case DO_PRE_DATA_BOUNDARY:
snprintf(buf, bufsize,
"PRE-DATA BOUNDARY (ID %d)",
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
new file mode 100644
index 9b6b9c2..db5c0e5
*** a/src/bin/psql/describe.c
--- b/src/bin/psql/describe.c
*************** listTables(const char *tabtypes, const c
*** 2698,2703 ****
--- 2698,2707 ----
appendPQExpBuffer(&buf,
",\n pg_catalog.obj_description(c.oid, 'pg_class') as \"%s\"",
gettext_noop("Description"));
+ if (pset.sversion >= 90300)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_get_expr(rs.rsecqual, c.oid) as \"%s\"",
+ gettext_noop("Row-security"));
}
appendPQExpBuffer(&buf,
*************** listTables(const char *tabtypes, const c
*** 2707,2712 ****
--- 2711,2719 ----
appendPQExpBuffer(&buf,
"\n LEFT JOIN pg_catalog.pg_index i ON i.indexrelid = c.oid"
"\n LEFT JOIN pg_catalog.pg_class c2 ON i.indrelid = c2.oid");
+ if (verbose && pset.sversion >= 90300)
+ appendPQExpBuffer(&buf,
+ "\n LEFT JOIN pg_rowsecurity rs ON rs.rsecrelid = c.oid");
appendPQExpBuffer(&buf, "\nWHERE c.relkind IN (");
if (showTables)
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
new file mode 100644
index 3aefbb5..c96e8d4
*** a/src/include/catalog/dependency.h
--- b/src/include/catalog/dependency.h
*************** typedef enum ObjectClass
*** 147,152 ****
--- 147,153 ----
OCLASS_DEFACL, /* pg_default_acl */
OCLASS_EXTENSION, /* pg_extension */
OCLASS_EVENT_TRIGGER, /* pg_event_trigger */
+ OCLASS_ROWSECURITY, /* pg_rowsecurity */
MAX_OCLASS /* MUST BE LAST */
} ObjectClass;
diff --git a/src/include/catalog/indexing.h b/src/include/catalog/indexing.h
new file mode 100644
index 19268fb..331a64b
*** a/src/include/catalog/indexing.h
--- b/src/include/catalog/indexing.h
*************** DECLARE_UNIQUE_INDEX(pg_extension_name_i
*** 311,316 ****
--- 311,321 ----
DECLARE_UNIQUE_INDEX(pg_range_rngtypid_index, 3542, on pg_range using btree(rngtypid oid_ops));
#define RangeTypidIndexId 3542
+ DECLARE_UNIQUE_INDEX(pg_rowsecurity_oid_index, 3819, on pg_rowsecurity using btree(oid oid_ops));
+ #define RowSecurityOidIndexId 3819
+ DECLARE_UNIQUE_INDEX(pg_rowsecurity_relid_index, 3839, on pg_rowsecurity using btree(rsecrelid oid_ops, rseccmd char_ops));
+ #define RowSecurityRelidIndexId 3839
+
/* last step of initialization script: build the indexes declared above */
BUILD_INDICES
diff --git a/src/include/catalog/pg_class.h b/src/include/catalog/pg_class.h
new file mode 100644
index 49c4f6f..6a16819
*** a/src/include/catalog/pg_class.h
--- b/src/include/catalog/pg_class.h
*************** CATALOG(pg_class,1259) BKI_BOOTSTRAP BKI
*** 64,69 ****
--- 64,70 ----
bool relhaspkey; /* has (or has had) PRIMARY KEY index */
bool relhasrules; /* has (or has had) any rules */
bool relhastriggers; /* has (or has had) any TRIGGERs */
+ bool relhasrowsecurity; /* has (or has had) row-security policy */
bool relhassubclass; /* has (or has had) derived classes */
bool relispopulated; /* matview currently holds query results */
TransactionId relfrozenxid; /* all Xids < this are frozen in this rel */
*************** typedef FormData_pg_class *Form_pg_class
*** 93,99 ****
* ----------------
*/
! #define Natts_pg_class 28
#define Anum_pg_class_relname 1
#define Anum_pg_class_relnamespace 2
#define Anum_pg_class_reltype 3
--- 94,100 ----
* ----------------
*/
! #define Natts_pg_class 29
#define Anum_pg_class_relname 1
#define Anum_pg_class_relnamespace 2
#define Anum_pg_class_reltype 3
*************** typedef FormData_pg_class *Form_pg_class
*** 116,127 ****
#define Anum_pg_class_relhaspkey 20
#define Anum_pg_class_relhasrules 21
#define Anum_pg_class_relhastriggers 22
! #define Anum_pg_class_relhassubclass 23
! #define Anum_pg_class_relispopulated 24
! #define Anum_pg_class_relfrozenxid 25
! #define Anum_pg_class_relminmxid 26
! #define Anum_pg_class_relacl 27
! #define Anum_pg_class_reloptions 28
/* ----------------
* initial contents of pg_class
--- 117,129 ----
#define Anum_pg_class_relhaspkey 20
#define Anum_pg_class_relhasrules 21
#define Anum_pg_class_relhastriggers 22
! #define Anum_pg_class_relhasrowsecurity 23
! #define Anum_pg_class_relhassubclass 24
! #define Anum_pg_class_relispopulated 25
! #define Anum_pg_class_relfrozenxid 26
! #define Anum_pg_class_relminmxid 27
! #define Anum_pg_class_relacl 28
! #define Anum_pg_class_reloptions 29
/* ----------------
* initial contents of pg_class
*************** typedef FormData_pg_class *Form_pg_class
*** 136,148 ****
* Note: "3" in the relfrozenxid column stands for FirstNormalTransactionId;
* similarly, "1" in relminmxid stands for FirstMultiXactId
*/
! DATA(insert OID = 1247 ( pg_type PGNSP 71 0 PGUID 0 0 0 0 0 0 0 f f p r 30 0 t f f f f t 3 1 _null_ _null_ ));
DESCR("");
! DATA(insert OID = 1249 ( pg_attribute PGNSP 75 0 PGUID 0 0 0 0 0 0 0 f f p r 21 0 f f f f f t 3 1 _null_ _null_ ));
DESCR("");
! DATA(insert OID = 1255 ( pg_proc PGNSP 81 0 PGUID 0 0 0 0 0 0 0 f f p r 27 0 t f f f f t 3 1 _null_ _null_ ));
DESCR("");
! DATA(insert OID = 1259 ( pg_class PGNSP 83 0 PGUID 0 0 0 0 0 0 0 f f p r 28 0 t f f f f t 3 1 _null_ _null_ ));
DESCR("");
--- 138,150 ----
* Note: "3" in the relfrozenxid column stands for FirstNormalTransactionId;
* similarly, "1" in relminmxid stands for FirstMultiXactId
*/
! DATA(insert OID = 1247 ( pg_type PGNSP 71 0 PGUID 0 0 0 0 0 0 0 f f p r 30 0 t f f f f f t 3 1 _null_ _null_ ));
DESCR("");
! DATA(insert OID = 1249 ( pg_attribute PGNSP 75 0 PGUID 0 0 0 0 0 0 0 f f p r 21 0 f f f f f f t 3 1 _null_ _null_ ));
DESCR("");
! DATA(insert OID = 1255 ( pg_proc PGNSP 81 0 PGUID 0 0 0 0 0 0 0 f f p r 27 0 t f f f f f t 3 1 _null_ _null_ ));
DESCR("");
! DATA(insert OID = 1259 ( pg_class PGNSP 83 0 PGUID 0 0 0 0 0 0 0 f f p r 29 0 t f f f f f t 3 1 _null_ _null_ ));
DESCR("");
diff --git a/src/include/catalog/pg_rowsecurity.h b/src/include/catalog/pg_rowsecurity.h
new file mode 100644
index ...a068b35
*** a/src/include/catalog/pg_rowsecurity.h
--- b/src/include/catalog/pg_rowsecurity.h
***************
*** 0 ****
--- 1,76 ----
+ /*
+ * pg_rowsecurity.h
+ * definition of the system catalog for row-security policy (pg_rowsecurity)
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ */
+ #ifndef PG_ROWSECURITY_H
+ #define PG_ROWSECURITY_H
+
+ #include "catalog/genbki.h"
+ #include "nodes/primnodes.h"
+ #include "utils/memutils.h"
+ #include "utils/relcache.h"
+
+ /* ----------------
+ * pg_rowlevelsec definition. cpp turns this into
+ * typedef struct FormData_pg_rowlevelsec
+ * ----------------
+ */
+ #define RowSecurityRelationId 3838
+
+ CATALOG(pg_rowsecurity,3838)
+ {
+ /* Oid of the relation that has row-security policy */
+ Oid rsecrelid;
+
+ /* One of ROWSECURITY_CMD_* below */
+ char rseccmd;
+ #ifdef CATALOG_VARLEN
+ pg_node_tree rsecqual;
+ #endif
+ } FormData_pg_rowsecurity;
+
+ /* ----------------
+ * Form_pg_rowlevelsec corresponds to a pointer to a row with
+ * the format of pg_rowlevelsec relation.
+ * ----------------
+ */
+ typedef FormData_pg_rowsecurity *Form_pg_rowsecurity;
+
+ /* ----------------
+ * compiler constants for pg_rowlevelsec
+ * ----------------
+ */
+ #define Natts_pg_rowsecurity 3
+ #define Anum_pg_rowsecurity_rsecrelid 1
+ #define Anum_pg_rowsecurity_rseccmd 2
+ #define Anum_pg_rowsecurity_rsecqual 3
+
+ #define ROWSECURITY_CMD_ALL 'a'
+ #define ROWSECURITY_CMD_SELECT 's'
+ #define ROWSECURITY_CMD_INSERT 'i'
+ #define ROWSECURITY_CMD_UPDATE 'u'
+ #define ROWSECURITY_CMD_DELETE 'd'
+
+ typedef struct
+ {
+ Oid rsecid;
+ Expr *qual;
+ bool hassublinks;
+ } RowSecurityEntry;
+
+ typedef struct
+ {
+ MemoryContext rscxt;
+ RowSecurityEntry rsall; /* row-security policy for ALL */
+ } RowSecurityDesc;
+
+ extern void RelationBuildRowSecurity(Relation relation);
+ extern void ATExecSetRowSecurity(Relation relation,
+ const char *cmdname, Node *clause);
+ extern void RemoveRowSecurityById(Oid relationId);
+
+ #endif /* PG_ROWSECURITY_H */
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
new file mode 100644
index edced29..bd14531
*** a/src/include/miscadmin.h
--- b/src/include/miscadmin.h
*************** extern int trace_recovery(int trace_leve
*** 279,284 ****
--- 279,285 ----
/* flags to be OR'd to form sec_context */
#define SECURITY_LOCAL_USERID_CHANGE 0x0001
#define SECURITY_RESTRICTED_OPERATION 0x0002
+ #define SECURITY_ROW_LEVEL_DISABLED 0x0004
extern char *DatabasePath;
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
new file mode 100644
index 298af26..93c40f2
*** a/src/include/nodes/execnodes.h
--- b/src/include/nodes/execnodes.h
*************** typedef struct JunkFilter
*** 308,313 ****
--- 308,315 ----
* ConstraintExprs array of constraint-checking expr states
* junkFilter for removing junk attributes from tuples
* projectReturning for computing a RETURNING list
+ * rowSecurity for row-security checks
+ * rowSecParams param-list if row-security has SubLink
* ----------------
*/
typedef struct ResultRelInfo
*************** typedef struct ResultRelInfo
*** 329,334 ****
--- 331,338 ----
List **ri_ConstraintExprs;
JunkFilter *ri_junkFilter;
ProjectionInfo *ri_projectReturning;
+ Node *ri_rowSecurity;
+ List *ri_rowSecParams;
} ResultRelInfo;
/* ----------------
diff --git a/src/include/nodes/nodeFuncs.h b/src/include/nodes/nodeFuncs.h
new file mode 100644
index d4901ca..cb30110
*** a/src/include/nodes/nodeFuncs.h
--- b/src/include/nodes/nodeFuncs.h
***************
*** 24,29 ****
--- 24,30 ----
#define QTW_IGNORE_RANGE_TABLE 0x08 /* skip rangetable entirely */
#define QTW_EXAMINE_RTES 0x10 /* examine RTEs */
#define QTW_DONT_COPY_QUERY 0x20 /* do not copy top Query */
+ #define QTW_IGNORE_RETURNING 0x40 /* skip returning clause */
extern Oid exprType(const Node *expr);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
new file mode 100644
index 9415e2c..7494b19
*** a/src/include/nodes/parsenodes.h
--- b/src/include/nodes/parsenodes.h
*************** typedef enum QuerySource
*** 31,37 ****
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 */
} QuerySource;
/* Sort ordering options for ORDER BY and CREATE INDEX */
--- 31,38 ----
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_ROW_SECURITY, /* added by row-security */
} QuerySource;
/* Sort ordering options for ORDER BY and CREATE INDEX */
*************** typedef struct Query
*** 112,118 ****
int resultRelation; /* rtable index of target relation for
* INSERT/UPDATE/DELETE; 0 for SELECT */
!
bool hasAggs; /* has aggregates in tlist or havingQual */
bool hasWindowFuncs; /* has window functions in tlist */
bool hasSubLinks; /* has subquery SubLink */
--- 113,121 ----
int resultRelation; /* rtable index of target relation for
* INSERT/UPDATE/DELETE; 0 for SELECT */
! int sourceRelation; /* rtable index of source relation for
! * UPDATE/DELETE, if not identical with
! * resultRelation; 0 for elsewhere */
bool hasAggs; /* has aggregates in tlist or havingQual */
bool hasWindowFuncs; /* has window functions in tlist */
bool hasSubLinks; /* has subquery SubLink */
*************** typedef struct RangeTblEntry
*** 727,732 ****
--- 730,740 ----
*/
Query *subquery; /* the sub-query */
bool security_barrier; /* is from security_barrier view? */
+ Oid rowsec_relid; /* OID of the original relation, if this
+ * sub-query originated from row-security
+ * policy on the relation. Elsewhere, it
+ * should be InvalidOid.
+ */
/*
* Fields valid for a join RTE (else NULL/zero):
*************** typedef enum AlterTableType
*** 1266,1271 ****
--- 1274,1281 ----
AT_DropInherit, /* NO INHERIT parent */
AT_AddOf, /* OF <type_name> */
AT_DropOf, /* NOT OF */
+ AT_SetRowSecurity, /* SET ROW SECURITY (...) */
+ AT_ResetRowSecurity, /* RESET ROW SECURITY */
AT_GenericOptions /* OPTIONS (...) */
} AlterTableType;
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
new file mode 100644
index aa4f12c..c040c23
*** a/src/include/nodes/plannodes.h
--- b/src/include/nodes/plannodes.h
*************** typedef struct PlannedStmt
*** 67,72 ****
--- 67,74 ----
List *invalItems; /* other dependencies, as PlanInvalItems */
int nParamExec; /* number of PARAM_EXEC Params used */
+
+ Oid planUserId; /* user-id this plan assumed, or InvalidOid */
} PlannedStmt;
/* macro for fetching the Plan associated with a SubPlan node */
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
new file mode 100644
index c0a636b..ed01b65
*** a/src/include/nodes/relation.h
--- b/src/include/nodes/relation.h
*************** typedef struct PlannerGlobal
*** 98,103 ****
--- 98,105 ----
Index lastRowMarkId; /* highest PlanRowMark ID assigned */
bool transientPlan; /* redo plan when TransactionXmin changes? */
+
+ Oid planUserId; /* User-Id to be assumed on this plan */
} PlannerGlobal;
/* macro for fetching the Plan associated with a SubPlan node */
*************** typedef struct AppendRelInfo
*** 1416,1421 ****
--- 1418,1427 ----
*/
Index parent_relid; /* RT index of append parent rel */
Index child_relid; /* RT index of append child rel */
+ Index child_result; /* RT index of append child rel's source,
+ * if source of result relation is not
+ * identical. Elsewhere, 0.
+ */
/*
* For an inheritance appendrel, the parent and child are both regular
diff --git a/src/include/optimizer/rowsecurity.h b/src/include/optimizer/rowsecurity.h
new file mode 100644
index ...ff4dd9a
*** a/src/include/optimizer/rowsecurity.h
--- b/src/include/optimizer/rowsecurity.h
***************
*** 0 ****
--- 1,27 ----
+ /* -------------------------------------------------------------------------
+ *
+ * rowsecurity.h
+ * prototypes for optimizer/rowsecurity.c
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * -------------------------------------------------------------------------
+ */
+ #ifndef ROWSECURITY_H
+ #define ROWSECURITY_H
+
+ #include "nodes/execnodes.h"
+ #include "nodes/parsenodes.h"
+ #include "nodes/relation.h"
+ #include "utils/rel.h"
+
+ typedef List *(*row_security_policy_hook_type)(CmdType cmdtype,
+ Relation relation);
+ extern PGDLLIMPORT row_security_policy_hook_type row_security_policy_hook;
+
+ extern bool copy_row_security_policy(CopyStmt *stmt,
+ Relation relation, List *attnums);
+ extern void apply_row_security_policy(PlannerInfo *root);
+
+ #endif /* ROWSECURITY_H */
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
new file mode 100644
index bea3b07..3910853
*** a/src/include/parser/parse_node.h
--- b/src/include/parser/parse_node.h
*************** typedef enum ParseExprKind
*** 63,69 ****
EXPR_KIND_INDEX_PREDICATE, /* index predicate */
EXPR_KIND_ALTER_COL_TRANSFORM, /* transform expr in ALTER COLUMN TYPE */
EXPR_KIND_EXECUTE_PARAMETER, /* parameter value in EXECUTE */
! EXPR_KIND_TRIGGER_WHEN /* WHEN condition in CREATE TRIGGER */
} ParseExprKind;
--- 63,70 ----
EXPR_KIND_INDEX_PREDICATE, /* index predicate */
EXPR_KIND_ALTER_COL_TRANSFORM, /* transform expr in ALTER COLUMN TYPE */
EXPR_KIND_EXECUTE_PARAMETER, /* parameter value in EXECUTE */
! EXPR_KIND_TRIGGER_WHEN, /* WHEN condition in CREATE TRIGGER */
! EXPR_KIND_ROW_SECURITY, /* ROW SECURITY policy for a table */
} ParseExprKind;
diff --git a/src/include/parser/parsetree.h b/src/include/parser/parsetree.h
new file mode 100644
index 9608cc6..b4e9df4
*** a/src/include/parser/parsetree.h
--- b/src/include/parser/parsetree.h
***************
*** 32,45 ****
((RangeTblEntry *) list_nth(rangetable, (rangetable_index)-1))
/*
- * getrelid
- *
* Given the range index of a relation, return the corresponding
* relation OID. Note that InvalidOid will be returned if the
* RTE is for a non-relation-type RTE.
*/
! #define getrelid(rangeindex,rangetable) \
! (rt_fetch(rangeindex, rangetable)->relid)
/*
* Given an RTE and an attribute number, return the appropriate
--- 32,42 ----
((RangeTblEntry *) list_nth(rangetable, (rangetable_index)-1))
/*
* Given the range index of a relation, return the corresponding
* relation OID. Note that InvalidOid will be returned if the
* RTE is for a non-relation-type RTE.
*/
! extern Oid getrelid(Index rangeindex, List *rangetable);
/*
* Given an RTE and an attribute number, return the appropriate
diff --git a/src/include/rewrite/rewriteHandler.h b/src/include/rewrite/rewriteHandler.h
new file mode 100644
index f0604b0..6b6c364
*** a/src/include/rewrite/rewriteHandler.h
--- b/src/include/rewrite/rewriteHandler.h
***************
*** 18,23 ****
--- 18,24 ----
#include "nodes/parsenodes.h"
extern List *QueryRewrite(Query *parsetree);
+ extern void QueryRewriteExpr(Node *node, List *activeRIRs);
extern void AcquireRewriteLocks(Query *parsetree, bool forUpdatePushedDown);
extern Node *build_column_default(Relation rel, int attrno);
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
new file mode 100644
index 72f8491..35819fa
*** a/src/include/utils/plancache.h
--- b/src/include/utils/plancache.h
*************** typedef struct CachedPlan
*** 128,133 ****
--- 128,135 ----
bool is_oneshot; /* is it a "oneshot" plan? */
bool is_saved; /* is CachedPlan in a long-lived context? */
bool is_valid; /* is the stmt_list currently valid? */
+ Oid planUserId; /* is user-id that is assumed on this cached
+ plan, or InvalidOid if portable for anybody */
TransactionId saved_xmin; /* if valid, replan when TransactionXmin
* changes from this value */
int generation; /* parent's generation number for this plan */
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
new file mode 100644
index 589c9a8..4e4ac65
*** a/src/include/utils/rel.h
--- b/src/include/utils/rel.h
***************
*** 18,23 ****
--- 18,24 ----
#include "catalog/pg_am.h"
#include "catalog/pg_class.h"
#include "catalog/pg_index.h"
+ #include "catalog/pg_rowsecurity.h"
#include "fmgr.h"
#include "nodes/bitmapset.h"
#include "rewrite/prs2lock.h"
*************** typedef struct RelationData
*** 109,114 ****
--- 110,116 ----
RuleLock *rd_rules; /* rewrite rules */
MemoryContext rd_rulescxt; /* private memory cxt for rd_rules, if any */
TriggerDesc *trigdesc; /* Trigger info, or NULL if rel has none */
+ RowSecurityDesc *rsdesc; /* Row-security policy, or NULL */
/*
* rd_options is set whenever rd_rel is loaded into the relcache entry.
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
new file mode 100644
index ...04591df
*** a/src/test/regress/expected/rowsecurity.out
--- b/src/test/regress/expected/rowsecurity.out
***************
*** 0 ****
--- 1,950 ----
+ --
+ -- Test of Row-level security feature
+ --
+ -- Clean up in case a prior regression run failed
+ -- Suppress NOTICE messages when users/groups don't exist
+ SET client_min_messages TO 'warning';
+ DROP USER IF EXISTS rls_regress_user0;
+ DROP USER IF EXISTS rls_regress_user1;
+ DROP USER IF EXISTS rls_regress_user2;
+ DROP SCHEMA IF EXISTS rls_regress_schema CASCADE;
+ RESET client_min_messages;
+ -- initial setup
+ CREATE USER rls_regress_user0;
+ CREATE USER rls_regress_user1;
+ CREATE USER rls_regress_user2;
+ CREATE SCHEMA rls_regress_schema;
+ GRANT ALL ON SCHEMA rls_regress_schema TO public;
+ SET search_path = rls_regress_schema;
+ -- setup of malicious function
+ CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool
+ COST 0.0000001 LANGUAGE plpgsql
+ AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END';
+ GRANT EXECUTE ON FUNCTION f_leak(text) TO public;
+ -- BASIC Row-Level Security Scenario
+ SET SESSION AUTHORIZATION rls_regress_user0;
+ CREATE TABLE uaccount (
+ pguser name primary key,
+ seclv int
+ );
+ INSERT INTO uaccount VALUES
+ ('rls_regress_user0', 99),
+ ('rls_regress_user1', 1),
+ ('rls_regress_user2', 2),
+ ('rls_regress_user3', 3);
+ GRANT SELECT ON uaccount TO public;
+ CREATE TABLE category (
+ cid int primary key,
+ cname text
+ );
+ GRANT ALL ON category TO public;
+ INSERT INTO category VALUES
+ (11, 'novel'),
+ (22, 'science fiction'),
+ (33, 'technology'),
+ (44, 'manga');
+ CREATE TABLE document (
+ did int primary key,
+ cid int references category(cid),
+ dlevel int not null,
+ dauthor name,
+ dtitle text
+ );
+ GRANT ALL ON document TO public;
+ INSERT INTO document VALUES
+ ( 1, 11, 1, 'rls_regress_user1', 'my first novel'),
+ ( 2, 11, 2, 'rls_regress_user1', 'my second novel'),
+ ( 3, 22, 2, 'rls_regress_user1', 'my science fiction'),
+ ( 4, 44, 1, 'rls_regress_user1', 'my first manga'),
+ ( 5, 44, 2, 'rls_regress_user1', 'my second manga'),
+ ( 6, 22, 1, 'rls_regress_user2', 'great science fiction'),
+ ( 7, 33, 2, 'rls_regress_user2', 'great technology book'),
+ ( 8, 44, 1, 'rls_regress_user2', 'great manga');
+ -- user's security level must higher than or equal to document's one
+ ALTER TABLE document SET ROW SECURITY FOR ALL
+ TO (dlevel <= (SELECT seclv FROM uaccount WHERE pguser = current_user));
+ -- viewpoint from rls_regress_user1
+ SET SESSION AUTHORIZATION rls_regress_user1;
+ SELECT * FROM document WHERE f_leak(dtitle);
+ NOTICE: f_leak => my first novel
+ NOTICE: f_leak => my first manga
+ NOTICE: f_leak => great science fiction
+ NOTICE: f_leak => great manga
+ did | cid | dlevel | dauthor | dtitle
+ -----+-----+--------+-------------------+-----------------------
+ 1 | 11 | 1 | rls_regress_user1 | my first novel
+ 4 | 44 | 1 | rls_regress_user1 | my first manga
+ 6 | 22 | 1 | rls_regress_user2 | great science fiction
+ 8 | 44 | 1 | rls_regress_user2 | great manga
+ (4 rows)
+
+ SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+ NOTICE: f_leak => my first novel
+ NOTICE: f_leak => my first manga
+ NOTICE: f_leak => great science fiction
+ NOTICE: f_leak => great manga
+ cid | did | dlevel | dauthor | dtitle | cname
+ -----+-----+--------+-------------------+-----------------------+-----------------
+ 11 | 1 | 1 | rls_regress_user1 | my first novel | novel
+ 22 | 6 | 1 | rls_regress_user2 | great science fiction | science fiction
+ 44 | 8 | 1 | rls_regress_user2 | great manga | manga
+ 44 | 4 | 1 | rls_regress_user1 | my first manga | manga
+ (4 rows)
+
+ -- viewpoint from rls_regress_user2
+ SET SESSION AUTHORIZATION rls_regress_user2;
+ SELECT * FROM document WHERE f_leak(dtitle);
+ NOTICE: f_leak => my first novel
+ NOTICE: f_leak => my second novel
+ NOTICE: f_leak => my science fiction
+ NOTICE: f_leak => my first manga
+ NOTICE: f_leak => my second manga
+ NOTICE: f_leak => great science fiction
+ NOTICE: f_leak => great technology book
+ NOTICE: f_leak => great manga
+ did | cid | dlevel | dauthor | dtitle
+ -----+-----+--------+-------------------+-----------------------
+ 1 | 11 | 1 | rls_regress_user1 | my first novel
+ 2 | 11 | 2 | rls_regress_user1 | my second novel
+ 3 | 22 | 2 | rls_regress_user1 | my science fiction
+ 4 | 44 | 1 | rls_regress_user1 | my first manga
+ 5 | 44 | 2 | rls_regress_user1 | my second manga
+ 6 | 22 | 1 | rls_regress_user2 | great science fiction
+ 7 | 33 | 2 | rls_regress_user2 | great technology book
+ 8 | 44 | 1 | rls_regress_user2 | great manga
+ (8 rows)
+
+ SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+ NOTICE: f_leak => my first novel
+ NOTICE: f_leak => my second novel
+ NOTICE: f_leak => my science fiction
+ NOTICE: f_leak => my first manga
+ NOTICE: f_leak => my second manga
+ NOTICE: f_leak => great science fiction
+ NOTICE: f_leak => great technology book
+ NOTICE: f_leak => great manga
+ cid | did | dlevel | dauthor | dtitle | cname
+ -----+-----+--------+-------------------+-----------------------+-----------------
+ 11 | 2 | 2 | rls_regress_user1 | my second novel | novel
+ 11 | 1 | 1 | rls_regress_user1 | my first novel | novel
+ 22 | 6 | 1 | rls_regress_user2 | great science fiction | science fiction
+ 22 | 3 | 2 | rls_regress_user1 | my science fiction | science fiction
+ 33 | 7 | 2 | rls_regress_user2 | great technology book | technology
+ 44 | 8 | 1 | rls_regress_user2 | great manga | manga
+ 44 | 5 | 2 | rls_regress_user1 | my second manga | manga
+ 44 | 4 | 1 | rls_regress_user1 | my first manga | manga
+ (8 rows)
+
+ EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+ QUERY PLAN
+ ----------------------------------------------------------
+ Subquery Scan on document
+ Filter: f_leak(document.dtitle)
+ -> Seq Scan on document document_1
+ Filter: (dlevel <= $0)
+ InitPlan 1 (returns $0)
+ -> Index Scan using uaccount_pkey on uaccount
+ Index Cond: (pguser = "current_user"())
+ (7 rows)
+
+ EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+ QUERY PLAN
+ ----------------------------------------------------------------------
+ Hash Join
+ Hash Cond: (category.cid = document.cid)
+ -> Seq Scan on category
+ -> Hash
+ -> Subquery Scan on document
+ Filter: f_leak(document.dtitle)
+ -> Seq Scan on document document_1
+ Filter: (dlevel <= $0)
+ InitPlan 1 (returns $0)
+ -> Index Scan using uaccount_pkey on uaccount
+ Index Cond: (pguser = "current_user"())
+ (11 rows)
+
+ -- only owner can change row-level security
+ ALTER TABLE document SET ROW SECURITY FOR ALL TO (true); -- fail
+ ERROR: must be owner of relation document
+ ALTER TABLE document RESET ROW SECURITY FOR ALL; -- fail
+ ERROR: must be owner of relation document
+ SET SESSION AUTHORIZATION rls_regress_user0;
+ ALTER TABLE document SET ROW SECURITY FOR ALL TO (dauthor = current_user);
+ -- viewpoint from rls_regress_user1 again
+ SET SESSION AUTHORIZATION rls_regress_user1;
+ SELECT * FROM document WHERE f_leak(dtitle);
+ NOTICE: f_leak => my first novel
+ NOTICE: f_leak => my second novel
+ NOTICE: f_leak => my science fiction
+ NOTICE: f_leak => my first manga
+ NOTICE: f_leak => my second manga
+ did | cid | dlevel | dauthor | dtitle
+ -----+-----+--------+-------------------+--------------------
+ 1 | 11 | 1 | rls_regress_user1 | my first novel
+ 2 | 11 | 2 | rls_regress_user1 | my second novel
+ 3 | 22 | 2 | rls_regress_user1 | my science fiction
+ 4 | 44 | 1 | rls_regress_user1 | my first manga
+ 5 | 44 | 2 | rls_regress_user1 | my second manga
+ (5 rows)
+
+ SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+ NOTICE: f_leak => my first novel
+ NOTICE: f_leak => my second novel
+ NOTICE: f_leak => my science fiction
+ NOTICE: f_leak => my first manga
+ NOTICE: f_leak => my second manga
+ cid | did | dlevel | dauthor | dtitle | cname
+ -----+-----+--------+-------------------+--------------------+-----------------
+ 11 | 1 | 1 | rls_regress_user1 | my first novel | novel
+ 11 | 2 | 2 | rls_regress_user1 | my second novel | novel
+ 22 | 3 | 2 | rls_regress_user1 | my science fiction | science fiction
+ 44 | 4 | 1 | rls_regress_user1 | my first manga | manga
+ 44 | 5 | 2 | rls_regress_user1 | my second manga | manga
+ (5 rows)
+
+ -- viewpoint from rls_regress_user2 again
+ SET SESSION AUTHORIZATION rls_regress_user2;
+ SELECT * FROM document WHERE f_leak(dtitle);
+ NOTICE: f_leak => great science fiction
+ NOTICE: f_leak => great technology book
+ NOTICE: f_leak => great manga
+ did | cid | dlevel | dauthor | dtitle
+ -----+-----+--------+-------------------+-----------------------
+ 6 | 22 | 1 | rls_regress_user2 | great science fiction
+ 7 | 33 | 2 | rls_regress_user2 | great technology book
+ 8 | 44 | 1 | rls_regress_user2 | great manga
+ (3 rows)
+
+ SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+ NOTICE: f_leak => great science fiction
+ NOTICE: f_leak => great technology book
+ NOTICE: f_leak => great manga
+ cid | did | dlevel | dauthor | dtitle | cname
+ -----+-----+--------+-------------------+-----------------------+-----------------
+ 22 | 6 | 1 | rls_regress_user2 | great science fiction | science fiction
+ 33 | 7 | 2 | rls_regress_user2 | great technology book | technology
+ 44 | 8 | 1 | rls_regress_user2 | great manga | manga
+ (3 rows)
+
+ EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+ QUERY PLAN
+ ----------------------------------------------
+ Subquery Scan on document
+ Filter: f_leak(document.dtitle)
+ -> Seq Scan on document document_1
+ Filter: (dauthor = "current_user"())
+ (4 rows)
+
+ EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+ QUERY PLAN
+ ----------------------------------------------------
+ Nested Loop
+ -> Subquery Scan on document
+ Filter: f_leak(document.dtitle)
+ -> Seq Scan on document document_1
+ Filter: (dauthor = "current_user"())
+ -> Index Scan using category_pkey on category
+ Index Cond: (cid = document.cid)
+ (7 rows)
+
+ -- interaction of FK/PK constraints
+ SET SESSION AUTHORIZATION rls_regress_user0;
+ ALTER TABLE category SET ROW SECURITY FOR ALL
+ TO (CASE WHEN current_user = 'rls_regress_user1' THEN cid IN (11, 33)
+ WHEN current_user = 'rls_regress_user2' THEN cid IN (22, 44)
+ ELSE false END);
+ -- cannot delete PK referenced by invisible FK
+ SET SESSION AUTHORIZATION rls_regress_user1;
+ SELECT * FROM document d full outer join category c on d.cid = c.cid;
+ did | cid | dlevel | dauthor | dtitle | cid | cname
+ -----+-----+--------+-------------------+--------------------+-----+------------
+ 2 | 11 | 2 | rls_regress_user1 | my second novel | 11 | novel
+ 1 | 11 | 1 | rls_regress_user1 | my first novel | 11 | novel
+ | | | | | 33 | technology
+ 5 | 44 | 2 | rls_regress_user1 | my second manga | |
+ 4 | 44 | 1 | rls_regress_user1 | my first manga | |
+ 3 | 22 | 2 | rls_regress_user1 | my science fiction | |
+ (6 rows)
+
+ DELETE FROM category WHERE cid = 33; -- failed
+ ERROR: update or delete on table "category" violates foreign key constraint "document_cid_fkey" on table "document"
+ DETAIL: Key (cid)=(33) is still referenced from table "document".
+ -- cannot insert FK referencing invisible PK
+ SET SESSION AUTHORIZATION rls_regress_user2;
+ SELECT * FROM document d full outer join category c on d.cid = c.cid;
+ did | cid | dlevel | dauthor | dtitle | cid | cname
+ -----+-----+--------+-------------------+-----------------------+-----+-----------------
+ 6 | 22 | 1 | rls_regress_user2 | great science fiction | 22 | science fiction
+ 8 | 44 | 1 | rls_regress_user2 | great manga | 44 | manga
+ 7 | 33 | 2 | rls_regress_user2 | great technology book | |
+ (3 rows)
+
+ INSERT INTO document VALUES (10, 33, 1, current_user, 'hoge'); -- failed
+ ERROR: insert or update on table "document" violates foreign key constraint "document_cid_fkey"
+ DETAIL: Key (cid)=(33) is not present in table "category".
+ -- database superuser can bypass RLS policy
+ RESET SESSION AUTHORIZATION;
+ SELECT * FROM document;
+ did | cid | dlevel | dauthor | dtitle
+ -----+-----+--------+-------------------+-----------------------
+ 1 | 11 | 1 | rls_regress_user1 | my first novel
+ 2 | 11 | 2 | rls_regress_user1 | my second novel
+ 3 | 22 | 2 | rls_regress_user1 | my science fiction
+ 4 | 44 | 1 | rls_regress_user1 | my first manga
+ 5 | 44 | 2 | rls_regress_user1 | my second manga
+ 6 | 22 | 1 | rls_regress_user2 | great science fiction
+ 7 | 33 | 2 | rls_regress_user2 | great technology book
+ 8 | 44 | 1 | rls_regress_user2 | great manga
+ (8 rows)
+
+ SELECT * FROM category;
+ cid | cname
+ -----+-----------------
+ 11 | novel
+ 22 | science fiction
+ 33 | technology
+ 44 | manga
+ (4 rows)
+
+ --
+ -- Table inheritance and RLS policy
+ --
+ SET SESSION AUTHORIZATION rls_regress_user0;
+ CREATE TABLE t1 (a int, junk1 text, b text) WITH OIDS;
+ ALTER TABLE t1 DROP COLUMN junk1; -- just a disturbing factor
+ GRANT ALL ON t1 TO public;
+ COPY t1 FROM stdin WITH (oids);
+ CREATE TABLE t2 (c float) INHERITS (t1);
+ COPY t2 FROM stdin WITH (oids);
+ CREATE TABLE t3 (c text, b text, a int) WITH OIDS;
+ ALTER TABLE t3 INHERIT t1;
+ COPY t3(a,b,c) FROM stdin WITH (oids);
+ ALTER TABLE t1 SET ROW SECURITY FOR ALL TO (a % 2 = 0); -- be even number
+ ALTER TABLE t2 SET ROW SECURITY FOR ALL TO (a % 2 = 1); -- be odd number
+ SELECT * FROM t1;
+ a | b
+ ---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+ (7 rows)
+
+ EXPLAIN (costs off) SELECT * FROM t1;
+ QUERY PLAN
+ -------------------------------------
+ Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+ (8 rows)
+
+ SELECT * FROM t1 WHERE f_leak(b);
+ NOTICE: f_leak => bbb
+ NOTICE: f_leak => ddd
+ NOTICE: f_leak => abc
+ NOTICE: f_leak => cde
+ NOTICE: f_leak => xxx
+ NOTICE: f_leak => yyy
+ NOTICE: f_leak => zzz
+ a | b
+ ---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+ (7 rows)
+
+ EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+ QUERY PLAN
+ -------------------------------------
+ Append
+ -> Subquery Scan on t1
+ Filter: f_leak(t1.b)
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ Filter: f_leak(t2.b)
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+ Filter: f_leak(b)
+ (11 rows)
+
+ -- reference to system column
+ SELECT oid, * FROM t1;
+ oid | a | b
+ -----+---+-----
+ 102 | 2 | bbb
+ 104 | 4 | ddd
+ 201 | 1 | abc
+ 203 | 3 | cde
+ 301 | 1 | xxx
+ 302 | 2 | yyy
+ 303 | 3 | zzz
+ (7 rows)
+
+ EXPLAIN (costs off) SELECT * FROM t1;
+ QUERY PLAN
+ -------------------------------------
+ Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+ (8 rows)
+
+ -- reference to whole-row reference
+ SELECT *,t1 FROM t1;
+ a | b | t1
+ ---+-----+---------
+ 2 | bbb | (2,bbb)
+ 4 | ddd | (4,ddd)
+ 1 | abc | (1,abc)
+ 3 | cde | (3,cde)
+ 1 | xxx | (1,xxx)
+ 2 | yyy | (2,yyy)
+ 3 | zzz | (3,zzz)
+ (7 rows)
+
+ EXPLAIN (costs off) SELECT *,t1 FROM t1;
+ QUERY PLAN
+ -------------------------------------
+ Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+ (8 rows)
+
+ -- for share/update lock
+ SELECT * FROM t1 FOR SHARE;
+ a | b
+ ---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+ (7 rows)
+
+ EXPLAIN (costs off) SELECT * FROM t1 FOR SHARE;
+ QUERY PLAN
+ -------------------------------------------
+ LockRows
+ -> Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+ (9 rows)
+
+ SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+ NOTICE: f_leak => bbb
+ NOTICE: f_leak => ddd
+ NOTICE: f_leak => abc
+ NOTICE: f_leak => cde
+ NOTICE: f_leak => xxx
+ NOTICE: f_leak => yyy
+ NOTICE: f_leak => zzz
+ a | b
+ ---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+ (7 rows)
+
+ EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+ QUERY PLAN
+ -------------------------------------------
+ LockRows
+ -> Append
+ -> Subquery Scan on t1
+ Filter: f_leak(t1.b)
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ Filter: f_leak(t2.b)
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+ Filter: f_leak(b)
+ (12 rows)
+
+ --
+ -- COPY TO statement
+ --
+ COPY t1 TO stdout;
+ 2 bbb
+ 4 ddd
+ COPY t1 TO stdout WITH OIDS;
+ 102 2 bbb
+ 104 4 ddd
+ COPY t2(c,b) TO stdout WITH OIDS;
+ 201 1.1 abc
+ 203 3.3 cde
+ COPY (SELECT * FROM t1) TO stdout;
+ 2 bbb
+ 4 ddd
+ 1 abc
+ 3 cde
+ 1 xxx
+ 2 yyy
+ 3 zzz
+ COPY document TO stdout WITH OIDS; -- failed (no oid column)
+ ERROR: table "document" does not have OIDs
+ --
+ -- recursive RLS and VIEWs in policy
+ --
+ CREATE TABLE s1 (a int, b text);
+ INSERT INTO s1 (SELECT x, md5(x::text) FROM generate_series(-10,10) x);
+ CREATE TABLE s2 (x int, y text);
+ INSERT INTO s2 (SELECT x, md5(x::text) FROM generate_series(-6,6) x);
+ CREATE VIEW v2 AS SELECT * FROM s2 WHERE y like '%af%';
+ ALTER TABLE s1 SET ROW SECURITY FOR ALL
+ TO (a in (select x from s2 where y like '%2f%'));
+ ALTER TABLE s2 SET ROW SECURITY FOR ALL
+ TO (x in (select a from s1 where b like '%22%'));
+ SELECT * FROM s1 WHERE f_leak(b); -- fail (infinite recursion)
+ ERROR: infinite recursion detected for relation "s1"
+ ALTER TABLE s2 SET ROW SECURITY FOR ALL TO (x % 2 = 0);
+ SELECT * FROM s1 WHERE f_leak(b); -- OK
+ NOTICE: f_leak => c81e728d9d4c2f636f067f89cc14862c
+ NOTICE: f_leak => a87ff679a2f3e71d9181a67b7542122c
+ a | b
+ ---+----------------------------------
+ 2 | c81e728d9d4c2f636f067f89cc14862c
+ 4 | a87ff679a2f3e71d9181a67b7542122c
+ (2 rows)
+
+ EXPLAIN SELECT * FROM only s1 WHERE f_leak(b);
+ QUERY PLAN
+ ---------------------------------------------------------------------------------------
+ Subquery Scan on s1 (cost=28.55..61.67 rows=205 width=36)
+ Filter: f_leak(s1.b)
+ -> Hash Join (cost=28.55..55.52 rows=615 width=36)
+ Hash Cond: (s1_1.a = s2.x)
+ -> Seq Scan on s1 s1_1 (cost=0.00..22.30 rows=1230 width=36)
+ -> Hash (cost=28.54..28.54 rows=1 width=4)
+ -> HashAggregate (cost=28.53..28.54 rows=1 width=4)
+ -> Subquery Scan on s2 (cost=0.00..28.52 rows=1 width=4)
+ Filter: (s2.y ~~ '%2f%'::text)
+ -> Seq Scan on s2 s2_1 (cost=0.00..28.45 rows=6 width=36)
+ Filter: ((x % 2) = 0)
+ (11 rows)
+
+ ALTER TABLE s1 SET ROW SECURITY FOR ALL
+ TO (a in (select x from v2)); -- using VIEW in RLS policy
+ SELECT * FROM s1 WHERE f_leak(b); -- OK
+ NOTICE: f_leak => 0267aaf632e87a63288a08331f22c7c3
+ NOTICE: f_leak => 1679091c5a880faf6fb5e6087eb1b2dc
+ a | b
+ ----+----------------------------------
+ -4 | 0267aaf632e87a63288a08331f22c7c3
+ 6 | 1679091c5a880faf6fb5e6087eb1b2dc
+ (2 rows)
+
+ EXPLAIN (COSTS OFF) SELECT * FROM s1 WHERE f_leak(b);
+ QUERY PLAN
+ ----------------------------------------------------------
+ Subquery Scan on s1
+ Filter: f_leak(s1.b)
+ -> Hash Join
+ Hash Cond: (s1_1.a = s2.x)
+ -> Seq Scan on s1 s1_1
+ -> Hash
+ -> HashAggregate
+ -> Subquery Scan on s2
+ Filter: (s2.y ~~ '%af%'::text)
+ -> Seq Scan on s2 s2_1
+ Filter: ((x % 2) = 0)
+ (11 rows)
+
+ SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+ xx | x | y
+ ----+----+----------------------------------
+ -6 | -6 | 596a3d04481816330f07e4f97510c28f
+ -4 | -4 | 0267aaf632e87a63288a08331f22c7c3
+ 2 | 2 | c81e728d9d4c2f636f067f89cc14862c
+ (3 rows)
+
+ EXPLAIN (COSTS OFF) SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+ QUERY PLAN
+ --------------------------------------------------------------------
+ Subquery Scan on s2
+ Filter: (s2.y ~~ '%28%'::text)
+ -> Seq Scan on s2 s2_1
+ Filter: ((x % 2) = 0)
+ SubPlan 1
+ -> Limit
+ -> Subquery Scan on s1
+ -> Nested Loop Semi Join
+ Join Filter: (s1_1.a = s2_2.x)
+ -> Seq Scan on s1 s1_1
+ -> Materialize
+ -> Subquery Scan on s2_2
+ Filter: (s2_2.y ~~ '%af%'::text)
+ -> Seq Scan on s2 s2_3
+ Filter: ((x % 2) = 0)
+ (15 rows)
+
+ ALTER TABLE s2 SET ROW SECURITY FOR ALL
+ TO (x in (select a from s1 where b like '%d2%'));
+ SELECT * FROM s1 WHERE f_leak(b); -- fail (infinite recursion via view)
+ ERROR: infinite recursion detected for relation "s1"
+ -- prepared statement with rls_regress_user0 privilege
+ PREPARE p1(int) AS SELECT * FROM t1 WHERE a <= $1;
+ EXECUTE p1(2);
+ a | b
+ ---+-----
+ 2 | bbb
+ 1 | abc
+ 1 | xxx
+ 2 | yyy
+ (4 rows)
+
+ EXPLAIN (costs off) EXECUTE p1(2);
+ QUERY PLAN
+ ----------------------------------------------------
+ Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a <= 2) AND ((a % 2) = 0))
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a <= 2) AND ((a % 2) = 1))
+ -> Seq Scan on t3
+ Filter: (a <= 2)
+ (9 rows)
+
+ -- superuser is allowed to bypass RLS checks
+ RESET SESSION AUTHORIZATION;
+ SELECT * FROM t1 WHERE f_leak(b);
+ NOTICE: f_leak => aaa
+ NOTICE: f_leak => bbb
+ NOTICE: f_leak => ccc
+ NOTICE: f_leak => ddd
+ NOTICE: f_leak => abc
+ NOTICE: f_leak => bcd
+ NOTICE: f_leak => cde
+ NOTICE: f_leak => def
+ NOTICE: f_leak => xxx
+ NOTICE: f_leak => yyy
+ NOTICE: f_leak => zzz
+ a | b
+ ---+-----
+ 1 | aaa
+ 2 | bbb
+ 3 | ccc
+ 4 | ddd
+ 1 | abc
+ 2 | bcd
+ 3 | cde
+ 4 | def
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+ (11 rows)
+
+ EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+ QUERY PLAN
+ ---------------------------
+ Append
+ -> Seq Scan on t1
+ Filter: f_leak(b)
+ -> Seq Scan on t2
+ Filter: f_leak(b)
+ -> Seq Scan on t3
+ Filter: f_leak(b)
+ (7 rows)
+
+ -- plan cache should be invalidated
+ EXECUTE p1(2);
+ a | b
+ ---+-----
+ 1 | aaa
+ 2 | bbb
+ 1 | abc
+ 2 | bcd
+ 1 | xxx
+ 2 | yyy
+ (6 rows)
+
+ EXPLAIN (costs off) EXECUTE p1(2);
+ QUERY PLAN
+ --------------------------
+ Append
+ -> Seq Scan on t1
+ Filter: (a <= 2)
+ -> Seq Scan on t2
+ Filter: (a <= 2)
+ -> Seq Scan on t3
+ Filter: (a <= 2)
+ (7 rows)
+
+ PREPARE p2(int) AS SELECT * FROM t1 WHERE a = $1;
+ EXECUTE p2(2);
+ a | b
+ ---+-----
+ 2 | bbb
+ 2 | bcd
+ 2 | yyy
+ (3 rows)
+
+ EXPLAIN (costs off) EXECUTE p2(2);
+ QUERY PLAN
+ -------------------------
+ Append
+ -> Seq Scan on t1
+ Filter: (a = 2)
+ -> Seq Scan on t2
+ Filter: (a = 2)
+ -> Seq Scan on t3
+ Filter: (a = 2)
+ (7 rows)
+
+ -- also, case when privilege switch from superuser
+ SET SESSION AUTHORIZATION rls_regress_user0;
+ EXECUTE p2(2);
+ a | b
+ ---+-----
+ 2 | bbb
+ 2 | yyy
+ (2 rows)
+
+ EXPLAIN (costs off) EXECUTE p2(2);
+ QUERY PLAN
+ ---------------------------------------------------
+ Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a = 2) AND ((a % 2) = 0))
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a = 2) AND ((a % 2) = 1))
+ -> Seq Scan on t3
+ Filter: (a = 2)
+ (9 rows)
+
+ --
+ -- UPDATE / DELETE and Row-level security
+ --
+ SET SESSION AUTHORIZATION rls_regress_user0;
+ EXPLAIN (costs off) UPDATE t1 SET b = b || b WHERE f_leak(b);
+ QUERY PLAN
+ -------------------------------------
+ Update on t1
+ -> Subquery Scan on t1_1
+ Filter: f_leak(t1_1.b)
+ -> Seq Scan on t1 t1_2
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ Filter: f_leak(t2.b)
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+ Filter: f_leak(b)
+ (11 rows)
+
+ UPDATE t1 SET b = b || b WHERE f_leak(b);
+ NOTICE: f_leak => bbb
+ NOTICE: f_leak => ddd
+ NOTICE: f_leak => abc
+ NOTICE: f_leak => cde
+ NOTICE: f_leak => xxx
+ NOTICE: f_leak => yyy
+ NOTICE: f_leak => zzz
+ EXPLAIN (costs off) UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b);
+ QUERY PLAN
+ -------------------------------------
+ Update on t1
+ -> Subquery Scan on t1_1
+ Filter: f_leak(t1_1.b)
+ -> Seq Scan on t1 t1_2
+ Filter: ((a % 2) = 0)
+ (5 rows)
+
+ UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b);
+ NOTICE: f_leak => bbbbbb
+ NOTICE: f_leak => dddddd
+ -- returning clause with system column
+ UPDATE only t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+ NOTICE: f_leak => bbbbbb_updt
+ NOTICE: f_leak => dddddd_updt
+ oid | a | b | t1
+ -----+---+-------------+-----------------
+ 102 | 2 | bbbbbb_updt | (2,bbbbbb_updt)
+ 104 | 4 | dddddd_updt | (4,dddddd_updt)
+ (2 rows)
+
+ UPDATE t1 SET b = b WHERE f_leak(b) RETURNING *;
+ NOTICE: f_leak => bbbbbb_updt
+ NOTICE: f_leak => dddddd_updt
+ NOTICE: f_leak => abcabc
+ NOTICE: f_leak => cdecde
+ NOTICE: f_leak => xxxxxx
+ NOTICE: f_leak => yyyyyy
+ NOTICE: f_leak => zzzzzz
+ a | b
+ ---+-------------
+ 2 | bbbbbb_updt
+ 4 | dddddd_updt
+ 1 | abcabc
+ 3 | cdecde
+ 1 | xxxxxx
+ 2 | yyyyyy
+ 3 | zzzzzz
+ (7 rows)
+
+ UPDATE t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+ NOTICE: f_leak => bbbbbb_updt
+ NOTICE: f_leak => dddddd_updt
+ NOTICE: f_leak => abcabc
+ NOTICE: f_leak => cdecde
+ NOTICE: f_leak => xxxxxx
+ NOTICE: f_leak => yyyyyy
+ NOTICE: f_leak => zzzzzz
+ oid | a | b | t1
+ -----+---+-------------+-----------------
+ 102 | 2 | bbbbbb_updt | (2,bbbbbb_updt)
+ 104 | 4 | dddddd_updt | (4,dddddd_updt)
+ 201 | 1 | abcabc | (1,abcabc)
+ 203 | 3 | cdecde | (3,cdecde)
+ 301 | 1 | xxxxxx | (1,xxxxxx)
+ 302 | 2 | yyyyyy | (2,yyyyyy)
+ 303 | 3 | zzzzzz | (3,zzzzzz)
+ (7 rows)
+
+ RESET SESSION AUTHORIZATION;
+ SELECT * FROM t1;
+ a | b
+ ---+-------------
+ 1 | aaa
+ 3 | ccc
+ 2 | bbbbbb_updt
+ 4 | dddddd_updt
+ 2 | bcd
+ 4 | def
+ 1 | abcabc
+ 3 | cdecde
+ 1 | xxxxxx
+ 2 | yyyyyy
+ 3 | zzzzzz
+ (11 rows)
+
+ SET SESSION AUTHORIZATION rls_regress_user0;
+ EXPLAIN (costs off) DELETE FROM only t1 WHERE f_leak(b);
+ QUERY PLAN
+ -------------------------------------
+ Delete on t1
+ -> Subquery Scan on t1_1
+ Filter: f_leak(t1_1.b)
+ -> Seq Scan on t1 t1_2
+ Filter: ((a % 2) = 0)
+ (5 rows)
+
+ EXPLAIN (costs off) DELETE FROM t1 WHERE f_leak(b);
+ QUERY PLAN
+ -------------------------------------
+ Delete on t1
+ -> Subquery Scan on t1_1
+ Filter: f_leak(t1_1.b)
+ -> Seq Scan on t1 t1_2
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ Filter: f_leak(t2.b)
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+ Filter: f_leak(b)
+ (11 rows)
+
+ DELETE FROM only t1 WHERE f_leak(b) RETURNING oid, *, t1;
+ NOTICE: f_leak => bbbbbb_updt
+ NOTICE: f_leak => dddddd_updt
+ oid | a | b | t1
+ -----+---+-------------+-----------------
+ 102 | 2 | bbbbbb_updt | (2,bbbbbb_updt)
+ 104 | 4 | dddddd_updt | (4,dddddd_updt)
+ (2 rows)
+
+ DELETE FROM t1 WHERE f_leak(b) RETURNING oid, *, t1;
+ NOTICE: f_leak => abcabc
+ NOTICE: f_leak => cdecde
+ NOTICE: f_leak => xxxxxx
+ NOTICE: f_leak => yyyyyy
+ NOTICE: f_leak => zzzzzz
+ oid | a | b | t1
+ -----+---+--------+------------
+ 201 | 1 | abcabc | (1,abcabc)
+ 203 | 3 | cdecde | (3,cdecde)
+ 301 | 1 | xxxxxx | (1,xxxxxx)
+ 302 | 2 | yyyyyy | (2,yyyyyy)
+ 303 | 3 | zzzzzz | (3,zzzzzz)
+ (5 rows)
+
+ --
+ -- Test psql \dt+ command
+ --
+ ALTER TABLE category RESET ROW SECURITY FOR ALL; -- too long qual
+ \dt+
+ List of relations
+ Schema | Name | Type | Owner | Size | Description | Row-security
+ --------------------+----------+-------+-------------------+------------+-------------+----------------------------------
+ rls_regress_schema | category | table | rls_regress_user0 | 16 kB | |
+ rls_regress_schema | document | table | rls_regress_user0 | 16 kB | | (dauthor = "current_user"())
+ rls_regress_schema | s1 | table | rls_regress_user0 | 16 kB | | (a IN ( SELECT v2.x +
+ | | | | | | FROM v2))
+ rls_regress_schema | s2 | table | rls_regress_user0 | 16 kB | | (x IN ( SELECT s1.a +
+ | | | | | | FROM s1 +
+ | | | | | | WHERE (s1.b ~~ '%d2%'::text)))
+ rls_regress_schema | t1 | table | rls_regress_user0 | 16 kB | | ((a % 2) = 0)
+ rls_regress_schema | t2 | table | rls_regress_user0 | 16 kB | | ((a % 2) = 1)
+ rls_regress_schema | t3 | table | rls_regress_user0 | 16 kB | |
+ rls_regress_schema | uaccount | table | rls_regress_user0 | 8192 bytes | |
+ (8 rows)
+
+ --
+ -- Clean up objects
+ --
+ RESET SESSION AUTHORIZATION;
+ DROP SCHEMA rls_regress_schema CASCADE;
+ NOTICE: drop cascades to 10 other objects
+ DETAIL: drop cascades to function f_leak(text)
+ drop cascades to table uaccount
+ drop cascades to table category
+ drop cascades to table document
+ drop cascades to table t1
+ drop cascades to table t2
+ drop cascades to table t3
+ drop cascades to table s1
+ drop cascades to table s2
+ drop cascades to view v2
+ DROP USER rls_regress_user0;
+ DROP USER rls_regress_user1;
+ DROP USER rls_regress_user2;
diff --git a/src/test/regress/expected/sanity_check.out b/src/test/regress/expected/sanity_check.out
new file mode 100644
index 432d39a..3b9123d
*** a/src/test/regress/expected/sanity_check.out
--- b/src/test/regress/expected/sanity_check.out
*************** SELECT relname, relhasindex
*** 120,125 ****
--- 120,126 ----
pg_proc | t
pg_range | t
pg_rewrite | t
+ pg_rowsecurity | t
pg_seclabel | t
pg_shdepend | t
pg_shdescription | t
*************** SELECT relname, relhasindex
*** 166,172 ****
timetz_tbl | f
tinterval_tbl | f
varchar_tbl | f
! (155 rows)
--
-- another sanity check: every system catalog that has OIDs should have
--- 167,173 ----
timetz_tbl | f
tinterval_tbl | f
varchar_tbl | f
! (156 rows)
--
-- another sanity check: every system catalog that has OIDs should have
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
new file mode 100644
index fd08e8d..367df00
*** a/src/test/regress/parallel_schedule
--- b/src/test/regress/parallel_schedule
*************** test: select_into select_distinct select
*** 83,89 ****
# ----------
# Another group of parallel tests
# ----------
! test: privileges security_label collate matview lock
# ----------
# Another group of parallel tests
--- 83,89 ----
# ----------
# Another group of parallel tests
# ----------
! test: privileges rowsecurity security_label collate matview lock
# ----------
# Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
new file mode 100644
index 1ed059b..5883a64
*** a/src/test/regress/serial_schedule
--- b/src/test/regress/serial_schedule
*************** test: delete
*** 93,98 ****
--- 93,99 ----
test: namespace
test: prepared_xacts
test: privileges
+ test: rowsecurity
test: security_label
test: collate
test: matview
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
new file mode 100644
index ...55d4aad
*** a/src/test/regress/sql/rowsecurity.sql
--- b/src/test/regress/sql/rowsecurity.sql
***************
*** 0 ****
--- 1,298 ----
+ --
+ -- Test of Row-level security feature
+ --
+
+ -- Clean up in case a prior regression run failed
+
+ -- Suppress NOTICE messages when users/groups don't exist
+ SET client_min_messages TO 'warning';
+
+ DROP USER IF EXISTS rls_regress_user0;
+ DROP USER IF EXISTS rls_regress_user1;
+ DROP USER IF EXISTS rls_regress_user2;
+
+ DROP SCHEMA IF EXISTS rls_regress_schema CASCADE;
+
+ RESET client_min_messages;
+
+ -- initial setup
+ CREATE USER rls_regress_user0;
+ CREATE USER rls_regress_user1;
+ CREATE USER rls_regress_user2;
+
+ CREATE SCHEMA rls_regress_schema;
+ GRANT ALL ON SCHEMA rls_regress_schema TO public;
+ SET search_path = rls_regress_schema;
+
+ -- setup of malicious function
+ CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool
+ COST 0.0000001 LANGUAGE plpgsql
+ AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END';
+ GRANT EXECUTE ON FUNCTION f_leak(text) TO public;
+
+ -- BASIC Row-Level Security Scenario
+
+ SET SESSION AUTHORIZATION rls_regress_user0;
+ CREATE TABLE uaccount (
+ pguser name primary key,
+ seclv int
+ );
+ INSERT INTO uaccount VALUES
+ ('rls_regress_user0', 99),
+ ('rls_regress_user1', 1),
+ ('rls_regress_user2', 2),
+ ('rls_regress_user3', 3);
+ GRANT SELECT ON uaccount TO public;
+
+ CREATE TABLE category (
+ cid int primary key,
+ cname text
+ );
+ GRANT ALL ON category TO public;
+ INSERT INTO category VALUES
+ (11, 'novel'),
+ (22, 'science fiction'),
+ (33, 'technology'),
+ (44, 'manga');
+
+ CREATE TABLE document (
+ did int primary key,
+ cid int references category(cid),
+ dlevel int not null,
+ dauthor name,
+ dtitle text
+ );
+ GRANT ALL ON document TO public;
+ INSERT INTO document VALUES
+ ( 1, 11, 1, 'rls_regress_user1', 'my first novel'),
+ ( 2, 11, 2, 'rls_regress_user1', 'my second novel'),
+ ( 3, 22, 2, 'rls_regress_user1', 'my science fiction'),
+ ( 4, 44, 1, 'rls_regress_user1', 'my first manga'),
+ ( 5, 44, 2, 'rls_regress_user1', 'my second manga'),
+ ( 6, 22, 1, 'rls_regress_user2', 'great science fiction'),
+ ( 7, 33, 2, 'rls_regress_user2', 'great technology book'),
+ ( 8, 44, 1, 'rls_regress_user2', 'great manga');
+
+ -- user's security level must higher than or equal to document's one
+ ALTER TABLE document SET ROW SECURITY FOR ALL
+ TO (dlevel <= (SELECT seclv FROM uaccount WHERE pguser = current_user));
+
+ -- viewpoint from rls_regress_user1
+ SET SESSION AUTHORIZATION rls_regress_user1;
+ SELECT * FROM document WHERE f_leak(dtitle);
+ SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+ -- viewpoint from rls_regress_user2
+ SET SESSION AUTHORIZATION rls_regress_user2;
+ SELECT * FROM document WHERE f_leak(dtitle);
+ SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+ EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+ EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+ -- only owner can change row-level security
+ ALTER TABLE document SET ROW SECURITY FOR ALL TO (true); -- fail
+ ALTER TABLE document RESET ROW SECURITY FOR ALL; -- fail
+
+ SET SESSION AUTHORIZATION rls_regress_user0;
+ ALTER TABLE document SET ROW SECURITY FOR ALL TO (dauthor = current_user);
+
+ -- viewpoint from rls_regress_user1 again
+ SET SESSION AUTHORIZATION rls_regress_user1;
+ SELECT * FROM document WHERE f_leak(dtitle);
+ SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+ -- viewpoint from rls_regress_user2 again
+ SET SESSION AUTHORIZATION rls_regress_user2;
+ SELECT * FROM document WHERE f_leak(dtitle);
+ SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+ EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+ EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+ -- interaction of FK/PK constraints
+ SET SESSION AUTHORIZATION rls_regress_user0;
+ ALTER TABLE category SET ROW SECURITY FOR ALL
+ TO (CASE WHEN current_user = 'rls_regress_user1' THEN cid IN (11, 33)
+ WHEN current_user = 'rls_regress_user2' THEN cid IN (22, 44)
+ ELSE false END);
+
+ -- cannot delete PK referenced by invisible FK
+ SET SESSION AUTHORIZATION rls_regress_user1;
+ SELECT * FROM document d full outer join category c on d.cid = c.cid;
+ DELETE FROM category WHERE cid = 33; -- failed
+
+ -- cannot insert FK referencing invisible PK
+ SET SESSION AUTHORIZATION rls_regress_user2;
+ SELECT * FROM document d full outer join category c on d.cid = c.cid;
+ INSERT INTO document VALUES (10, 33, 1, current_user, 'hoge'); -- failed
+
+ -- database superuser can bypass RLS policy
+ RESET SESSION AUTHORIZATION;
+ SELECT * FROM document;
+ SELECT * FROM category;
+
+ --
+ -- Table inheritance and RLS policy
+ --
+ SET SESSION AUTHORIZATION rls_regress_user0;
+
+ CREATE TABLE t1 (a int, junk1 text, b text) WITH OIDS;
+ ALTER TABLE t1 DROP COLUMN junk1; -- just a disturbing factor
+ GRANT ALL ON t1 TO public;
+
+ COPY t1 FROM stdin WITH (oids);
+ 101 1 aaa
+ 102 2 bbb
+ 103 3 ccc
+ 104 4 ddd
+ \.
+
+ CREATE TABLE t2 (c float) INHERITS (t1);
+ COPY t2 FROM stdin WITH (oids);
+ 201 1 abc 1.1
+ 202 2 bcd 2.2
+ 203 3 cde 3.3
+ 204 4 def 4.4
+ \.
+
+ CREATE TABLE t3 (c text, b text, a int) WITH OIDS;
+ ALTER TABLE t3 INHERIT t1;
+ COPY t3(a,b,c) FROM stdin WITH (oids);
+ 301 1 xxx X
+ 302 2 yyy Y
+ 303 3 zzz Z
+ \.
+
+ ALTER TABLE t1 SET ROW SECURITY FOR ALL TO (a % 2 = 0); -- be even number
+ ALTER TABLE t2 SET ROW SECURITY FOR ALL TO (a % 2 = 1); -- be odd number
+
+ SELECT * FROM t1;
+ EXPLAIN (costs off) SELECT * FROM t1;
+
+ SELECT * FROM t1 WHERE f_leak(b);
+ EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+
+ -- reference to system column
+ SELECT oid, * FROM t1;
+ EXPLAIN (costs off) SELECT * FROM t1;
+
+ -- reference to whole-row reference
+ SELECT *,t1 FROM t1;
+ EXPLAIN (costs off) SELECT *,t1 FROM t1;
+
+ -- for share/update lock
+ SELECT * FROM t1 FOR SHARE;
+ EXPLAIN (costs off) SELECT * FROM t1 FOR SHARE;
+
+ SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+ EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+
+ --
+ -- COPY TO statement
+ --
+ COPY t1 TO stdout;
+ COPY t1 TO stdout WITH OIDS;
+ COPY t2(c,b) TO stdout WITH OIDS;
+ COPY (SELECT * FROM t1) TO stdout;
+ COPY document TO stdout WITH OIDS; -- failed (no oid column)
+
+ --
+ -- recursive RLS and VIEWs in policy
+ --
+ CREATE TABLE s1 (a int, b text);
+ INSERT INTO s1 (SELECT x, md5(x::text) FROM generate_series(-10,10) x);
+
+ CREATE TABLE s2 (x int, y text);
+ INSERT INTO s2 (SELECT x, md5(x::text) FROM generate_series(-6,6) x);
+ CREATE VIEW v2 AS SELECT * FROM s2 WHERE y like '%af%';
+
+ ALTER TABLE s1 SET ROW SECURITY FOR ALL
+ TO (a in (select x from s2 where y like '%2f%'));
+
+ ALTER TABLE s2 SET ROW SECURITY FOR ALL
+ TO (x in (select a from s1 where b like '%22%'));
+
+ SELECT * FROM s1 WHERE f_leak(b); -- fail (infinite recursion)
+
+ ALTER TABLE s2 SET ROW SECURITY FOR ALL TO (x % 2 = 0);
+
+ SELECT * FROM s1 WHERE f_leak(b); -- OK
+ EXPLAIN SELECT * FROM only s1 WHERE f_leak(b);
+
+ ALTER TABLE s1 SET ROW SECURITY FOR ALL
+ TO (a in (select x from v2)); -- using VIEW in RLS policy
+ SELECT * FROM s1 WHERE f_leak(b); -- OK
+ EXPLAIN (COSTS OFF) SELECT * FROM s1 WHERE f_leak(b);
+
+ SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+ EXPLAIN (COSTS OFF) SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+
+ ALTER TABLE s2 SET ROW SECURITY FOR ALL
+ TO (x in (select a from s1 where b like '%d2%'));
+ SELECT * FROM s1 WHERE f_leak(b); -- fail (infinite recursion via view)
+
+ -- prepared statement with rls_regress_user0 privilege
+ PREPARE p1(int) AS SELECT * FROM t1 WHERE a <= $1;
+ EXECUTE p1(2);
+ EXPLAIN (costs off) EXECUTE p1(2);
+
+ -- superuser is allowed to bypass RLS checks
+ RESET SESSION AUTHORIZATION;
+ SELECT * FROM t1 WHERE f_leak(b);
+ EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+
+ -- plan cache should be invalidated
+ EXECUTE p1(2);
+ EXPLAIN (costs off) EXECUTE p1(2);
+
+ PREPARE p2(int) AS SELECT * FROM t1 WHERE a = $1;
+ EXECUTE p2(2);
+ EXPLAIN (costs off) EXECUTE p2(2);
+
+ -- also, case when privilege switch from superuser
+ SET SESSION AUTHORIZATION rls_regress_user0;
+ EXECUTE p2(2);
+ EXPLAIN (costs off) EXECUTE p2(2);
+
+ --
+ -- UPDATE / DELETE and Row-level security
+ --
+ SET SESSION AUTHORIZATION rls_regress_user0;
+ EXPLAIN (costs off) UPDATE t1 SET b = b || b WHERE f_leak(b);
+ UPDATE t1 SET b = b || b WHERE f_leak(b);
+
+ EXPLAIN (costs off) UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b);
+ UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b);
+
+ -- returning clause with system column
+ UPDATE only t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+ UPDATE t1 SET b = b WHERE f_leak(b) RETURNING *;
+ UPDATE t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+
+ RESET SESSION AUTHORIZATION;
+ SELECT * FROM t1;
+
+ SET SESSION AUTHORIZATION rls_regress_user0;
+ EXPLAIN (costs off) DELETE FROM only t1 WHERE f_leak(b);
+ EXPLAIN (costs off) DELETE FROM t1 WHERE f_leak(b);
+
+ DELETE FROM only t1 WHERE f_leak(b) RETURNING oid, *, t1;
+ DELETE FROM t1 WHERE f_leak(b) RETURNING oid, *, t1;
+
+ --
+ -- Test psql \dt+ command
+ --
+ ALTER TABLE category RESET ROW SECURITY FOR ALL; -- too long qual
+ \dt+
+
+ --
+ -- Clean up objects
+ --
+ RESET SESSION AUTHORIZATION;
+
+ DROP SCHEMA rls_regress_schema CASCADE;
+
+ DROP USER rls_regress_user0;
+ DROP USER rls_regress_user1;
+ DROP USER rls_regress_user2;
* Greg Smith (greg@2ndQuadrant.com) wrote:
On 7/18/13 7:57 PM, Karol Trzcionka wrote:
Current head 4cbe3ac3e86790d05c569de4585e5075a62a9b41 -> patch applies
correct (only change needed in parallel_schedule).
However it fails on own regression tests (other tests pass).I got a rejected hunk in src/backend/nodes/nodeFuncs.c as well as
that parallel_schedule issue. Maybe you didn't get the nodeFuncs
change but didn't notice that? That might explain why the tests
didn't work for you either.
The nodeFuncs.c hunk seems likely to have been impacted by the patch I
committed today (WITH CHECK OPTION), so I doubt that was the issue.
Attached is an updated patch where I tried to only fix the two small
hunks of bit rot. I get "All 140 tests passed" here, on a Mac no
less.
Thanks for updating the patch, I ran into the failed hunks too and
expected to have to deal with them. :)
I did a brief code scan through the patch just to get a feel for how
the feature is put together, and what you'd need to know for a
deeper review.
That would be extremely helpful.. Wasn't there a wiki page about this
feature which might also help? Bigger question- is it correct for the
latest version of the patch?
(I'm trying to get customer time approved to work on
this a lot more) The code was easier to follow than I expected.
The way it completely avoids even getting into the security label
integration yet seems like a successful design partitioning. This
isn't nearly as scary as the SEPostgres patches. There are some
useful looking utility functions that dump information about what's
going on too.The bulk of the complexity is how the feature modifies query nodes
to restrict what rows come through them. Some familiarity with that
part of the code is what you'd need to take on reviewing this in
detail. That and a week of time to spend trudging through it. If
anyone is looking for an educational challenge on query execution,
marching through all of these changes to validate they work as
expected would do that.
I'm hoping to find time this weekend to look into this patch myself, but
the weekend is also filling up with other activities, so we'll see.
Thanks!
Stephen
On 7/18/13 11:03 PM, Stephen Frost wrote:
Wasn't there a wiki page about this
feature which might also help? Bigger question- is it correct for the
latest version of the patch?
https://wiki.postgresql.org/wiki/RLS has accumulated a lot of older
debris that may or may not be useful here.
This useful piece was just presented at PGCon:
http://www.pgcon.org/2013/schedule/attachments/273_PGcon2013-kaigai-row-level-security.pdf
That is very up to date intro to the big picture issues.
--
Greg Smith 2ndQuadrant US greg@2ndQuadrant.com Baltimore, MD
PostgreSQL Training, Services, and 24x7 Support www.2ndQuadrant.com
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 19 July 2013 04:09, Greg Smith <greg@2ndquadrant.com> wrote:
On 7/18/13 11:03 PM, Stephen Frost wrote:
Wasn't there a wiki page about this
feature which might also help? Bigger question- is it correct for the
latest version of the patch?https://wiki.postgresql.org/wiki/RLS has accumulated a lot of older debris
that may or may not be useful here.This useful piece was just presented at PGCon:
http://www.pgcon.org/2013/schedule/attachments/273_PGcon2013-kaigai-row-level-security.pdf
That is very up to date intro to the big picture issues.
Hi,
I took a look at this patch too. I didn't read all the code in detail,
but there was one area I wondered about --- is it still necessary to
add the new field rowsec_relid to RangeTblEntry?
As far as I can see, it is only used in the new function getrelid()
which replaces the old macro of the same name, so that it can work if
it is passed the index of the sourceRelation subquery RTE instead of
the resultRelation. This seems to be encoding a specific assumption
that a subquery RTE is always going to come from row-level security
expansion.
Is it the case that this can only happen from expand_targetlist(), in
which case why not pass both source_relation and result_relation to
expand_targetlist(), which I think will make that code neater. As it
stands, what expand_targetlist() sees as result_relation is actually
source_relation, which getrelid() then handles specially. Logically I
think expand_targetlist() should pass the actual result_relation to
getrelid(), opening the target table, and then pass source_relation to
makeVar() when building new TLEs.
With that change to expand_targetlist(), the change to getrelid() may
be unnecessary, and then also the new rowsec_relid field in
RangeTblEntry may not be needed.
Regards,
Dean
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Dean Rasheed <dean.a.rasheed@gmail.com> writes:
I took a look at this patch too. I didn't read all the code in detail,
but there was one area I wondered about --- is it still necessary to
add the new field rowsec_relid to RangeTblEntry?
As far as I can see, it is only used in the new function getrelid()
which replaces the old macro of the same name, so that it can work if
it is passed the index of the sourceRelation subquery RTE instead of
the resultRelation. This seems to be encoding a specific assumption
that a subquery RTE is always going to come from row-level security
expansion.
I haven't read the patch at all, but I would opine that anything that's
changing the behavior of getrelid() is broken by definition. Something
is being done at the wrong level of abstraction if you think that you
need to do that.
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
2013/7/19 Stephen Frost <sfrost@snowman.net>:
* Greg Smith (greg@2ndQuadrant.com) wrote:
On 7/18/13 7:57 PM, Karol Trzcionka wrote:
Current head 4cbe3ac3e86790d05c569de4585e5075a62a9b41 -> patch applies
correct (only change needed in parallel_schedule).
However it fails on own regression tests (other tests pass).I got a rejected hunk in src/backend/nodes/nodeFuncs.c as well as
that parallel_schedule issue. Maybe you didn't get the nodeFuncs
change but didn't notice that? That might explain why the tests
didn't work for you either.The nodeFuncs.c hunk seems likely to have been impacted by the patch I
committed today (WITH CHECK OPTION), so I doubt that was the issue.Attached is an updated patch where I tried to only fix the two small
hunks of bit rot. I get "All 140 tests passed" here, on a Mac no
less.Thanks for updating the patch, I ran into the failed hunks too and
expected to have to deal with them. :)
Thanks for pointing out this problem. I synchronized my local master
with the upstream one, then adjusted the row-security branch.
I'll submit the patch soon, with fixing-up the portion that Tom pointed
out.
Thanks,
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
2013/7/19 Dean Rasheed <dean.a.rasheed@gmail.com>:
On 19 July 2013 04:09, Greg Smith <greg@2ndquadrant.com> wrote:
On 7/18/13 11:03 PM, Stephen Frost wrote:
Wasn't there a wiki page about this
feature which might also help? Bigger question- is it correct for the
latest version of the patch?https://wiki.postgresql.org/wiki/RLS has accumulated a lot of older debris
that may or may not be useful here.This useful piece was just presented at PGCon:
http://www.pgcon.org/2013/schedule/attachments/273_PGcon2013-kaigai-row-level-security.pdf
That is very up to date intro to the big picture issues.Hi,
I took a look at this patch too. I didn't read all the code in detail,
but there was one area I wondered about --- is it still necessary to
add the new field rowsec_relid to RangeTblEntry?As far as I can see, it is only used in the new function getrelid()
which replaces the old macro of the same name, so that it can work if
it is passed the index of the sourceRelation subquery RTE instead of
the resultRelation. This seems to be encoding a specific assumption
that a subquery RTE is always going to come from row-level security
expansion.Is it the case that this can only happen from expand_targetlist(), in
which case why not pass both source_relation and result_relation to
expand_targetlist(), which I think will make that code neater. As it
stands, what expand_targetlist() sees as result_relation is actually
source_relation, which getrelid() then handles specially. Logically I
think expand_targetlist() should pass the actual result_relation to
getrelid(), opening the target table, and then pass source_relation to
makeVar() when building new TLEs.With that change to expand_targetlist(), the change to getrelid() may
be unnecessary, and then also the new rowsec_relid field in
RangeTblEntry may not be needed.
Hmm. I didn't have this idea. It seems to me fair enough and kills
necessity to enhance RangeTblEntry and getrelid() indeed.
I try to fix up this implementation according to your suggestion.
Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 7/20/13 10:08 AM, Kohei KaiGai wrote:
With that change to expand_targetlist(), the change to getrelid() may
be unnecessary, and then also the new rowsec_relid field in
RangeTblEntry may not be needed.Hmm. I didn't have this idea. It seems to me fair enough and kills
necessity to enhance RangeTblEntry and getrelid() indeed.
I try to fix up this implementation according to your suggestion.
Great, there's one useful bit of feedback for you then, and that seems
to address Tom's getrelid concern too.
For the active CommitFest, I don't see any place we can go with this
right now except for "Returned with Feedback". We really need more
reviewers willing to put a significant amount of time into going through
this code.
Anyone who would like to see RLS committed should consider how you can
get resources to support a skilled PostgreSQL reviewer spending time on
it. (This is a bit much to expect new reviewers to chew on usefully)
I've been working on that here, but I don't have anything I can publicly
commit to yet.
--
Greg Smith 2ndQuadrant US greg@2ndQuadrant.com Baltimore, MD
PostgreSQL Training, Services, and 24x7 Support www.2ndQuadrant.com
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
(This is a bit much to expect new reviewers to chew on usefully) I've been
working on that here, but I don't have anything I can publicly commit to
yet.
True that.
I spent some time on it, but couldn't come up with anything useful.
Mike's extensive testing seems good enough for me, though.
One thing I was a bit concerned about (I don't know if it has been
resolved already, or if it isn't a concern) is that there was an issue
about a security part being visible in the syntax (or something like
that, my memory isn't too good today morning).
Regards,
Atri
Regards,
Atri
l'apprenant
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 07/22/2013 01:27 PM, Greg Smith wrote:
Anyone who would like to see RLS committed should consider how you can
get resources to support a skilled PostgreSQL reviewer spending time on
it. (This is a bit much to expect new reviewers to chew on usefully)
I've been working on that here, but I don't have anything I can publicly
commit to yet.
Apparently it's a little much for experienced reviewers to chew on, too,
since I've been trying to get someone to review it since the beginning
of the Commitfest.
While I understand the call for "resources", this is a bit unfair to
KaiGai, who has put in his time reviewing other people's patches.
--
Josh Berkus
PostgreSQL Experts Inc.
http://pgexperts.com
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Import Notes
Reply to msg id not found: WMc1afed1e22ee997691605f7faaf15e25b70edf09cbfb322a09a707e96db70172e2bd8984779fa5ae1c976322b8ceedf5@asav-2.01.com
On 7/23/13 12:10 PM, Josh Berkus wrote:
Apparently it's a little much for experienced reviewers to chew on, too,
since I've been trying to get someone to review it since the beginning
of the Commitfest.
It's more than the available experienced reviewers are willing to chew
on fully as volunteers. The reward for spending review time is pretty
low right now.
While I understand the call for "resources", this is a bit unfair to
KaiGai, who has put in his time reviewing other people's patches.
If you read Dean Rasheed's comments, it's obvious he put a useful amount
of work into his review suggestions. It is not the case here that
KaiGai worked on a review and got nothing in return. Unfortunately that
has happened to this particular patch before, but the community did a
little better this time.
The goal of the CF is usually to reach one of these outcomes for every
submission:
-The submission is ready for commit
-The submission needs improvement in X
Review here went far enough to identify an X to be improved. It was a
big enough issue that a rewrite is needed, commit at this time isn't
possible, and now KaiGai has something we hope is productive he can
continue working on. That's all we can really promise here--that if we
say something isn't ready for commit yet, that there's some feedback as
to why.
I would have preferred to see multiple X issues identified here, given
that we know there has to be more than just the one in a submission of
this size. The rough fairness promises of the CommitFest seem satisfied
to me though.
--
Greg Smith 2ndQuadrant US greg@2ndQuadrant.com Baltimore, MD
PostgreSQL Training, Services, and 24x7 Support www.2ndQuadrant.com
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Greg,
It's more than the available experienced reviewers are willing to chew
on fully as volunteers. The reward for spending review time is pretty
low right now.
Short of paying for review time, I don't think we have another solution
for getting the "big patches" reviewed, except to rely on the major
contributors who are paid full-time to hack Postgres. You know as well
as me that, as consultants, we can get clients to pay for 10% extra time
for review in the course of developing a feature, but the kind of time
which patches like Row Security, Changesets, or other "big patches" need
nobody is going to pay for on a contract basis. And nobody who is doing
this in their "spare time" has that kind of block.
So I don't think there's any good solution for the "big patches".
I do think our project could do much more to recruit reviewers for the
small-medium patches, to take workload off the core contributors in
general. Historically, however, this project (and the contributors on
this list) has made material decisions not to encourage or recruit new
people as reviewers, and has repeatedly stated that reviewers are not
important. Until that changes, we are not going to get more reviewers
(and I'm not going to have much sympathy for existing contributors who
say they have no time for review).
If we want more reviewers and people spending more time on review, then
we need to give reviewers the *same* respect and the *same* rewards that
feature contributors get. Not something else, the exact same.
--
Josh Berkus
PostgreSQL Experts Inc.
http://pgexperts.com
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Import Notes
Reply to msg id not found: WMbb6c4ef74149fd41bfefac93555993908116cd94a30071c92b56e85657029e20898538a11e44a85ffef2d5500c625eb0@asav-2.01.com
On 7/23/13 2:30 PM, Josh Berkus wrote:
You know as well as me that, as consultants, we can get clients to pay for 10% extra time
for review in the course of developing a feature
Before this number gets quoted anywhere, I think it's on the low side.
I've spent a good bit of time measuring how much time it takes to do a
fair offsetting review--one where you put as much time in as it takes to
review your submission--and I keep getting numbers more in the 20 to 25%
range. The work involved to do a high quality review takes a while.
I happen to think the review structure is one of the most important
components to PostgreSQL release quality. It used to be a single level
review with just the committers, now it's a two level structure. The
reason the Postgres code is so good isn't that the submitted development
is inherently any better than other projects. There's plenty of bogus
material submitted here. It's the high standards for review and commit
that are the key filter. The importance of the process to the result
isn't weighed as heavily as I think it should be.
--
Greg Smith 2ndQuadrant US greg@2ndQuadrant.com Baltimore, MD
PostgreSQL Training, Services, and 24x7 Support www.2ndQuadrant.com
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 07/23/2013 03:34 PM, Greg Smith wrote:
I happen to think the review structure is one of the most important
components to PostgreSQL release quality. It used to be a single level
review with just the committers, now it's a two level structure. The
reason the Postgres code is so good isn't that the submitted development
is inherently any better than other projects. There's plenty of bogus
material submitted here. It's the high standards for review and commit
that are the key filter. The importance of the process to the result
isn't weighed as heavily as I think it should be.
I think we're in violent agreement here. Now, we just need to convince
everyone else on this list.
--
Josh Berkus
PostgreSQL Experts Inc.
http://pgexperts.com
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Import Notes
Reply to msg id not found: WM1ca7ff29006eb68455f28923e9fb501ab51f947f5fd292721d76ee64b1b5824b9d953b2012710caef9758b745245da3f@asav-2.01.com
On Tue, Jul 23, 2013 at 11:30:14AM -0700, Josh Berkus wrote:
Greg,
It's more than the available experienced reviewers are willing to chew
on fully as volunteers. The reward for spending review time is pretty
low right now.Short of paying for review time, I don't think we have another solution
for getting the "big patches" reviewed, except to rely on the major
contributors who are paid full-time to hack Postgres. You know as well
as me that, as consultants, we can get clients to pay for 10% extra time
for review in the course of developing a feature, but the kind of time
which patches like Row Security, Changesets, or other "big patches" need
nobody is going to pay for on a contract basis. And nobody who is doing
this in their "spare time" has that kind of block.So I don't think there's any good solution for the "big patches".
Let me echo Josh's comments above --- in the early years, we had trouble
creating new features that required more than 1-2 weekends of
development. We now have enough full-time developers that this is not a
problem, but now it seems features requiring more than a weekend to
_review_ are a problem, so full-time folks are again required here.
--
Bruce Momjian <bruce@momjian.us> http://momjian.us
EnterpriseDB http://enterprisedb.com
+ It's impossible for everything to be true. +
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 7/20/13 10:08 AM, Kohei KaiGai wrote:
Hmm. I didn't have this idea. It seems to me fair enough and kills
necessity to enhance RangeTblEntry and getrelid() indeed.
I try to fix up this implementation according to your suggestion.
How is that going? I'm going to do a serious review of this myself over
the next few weeks. I have a good chunk of time set aside for it as
part of a larger project. I'm hoping to get more people here involved
in that effort too, starting in the November CF if that works out.
I've been trying to catch up with your larger plan for this feature for
9.4. You made this comment earlier:
Also, I'd like to have discussion for this feature in earlier half of
v9.4 to keep time for the remaining features, such as check on
writer-side, integration with selinux, and so on
Is any of that code around yet? I see that you have split your
submissions so that a smaller program can be reviewed today. I'd like
to start taking a look at the next step too though. For the project I'm
starting to work on here, getting the integration with labeling also
done is a very important thing to target for 9.4. It would be nice to
see how that fits together today, even if the code for it isn't being
reviewed heavily yet.
I don't quite understand yet what's missing on the writer side. If you
could help explain what's missing there, I would like to read about that.
--
Greg Smith 2ndQuadrant US greg@2ndQuadrant.com Baltimore, MD
PostgreSQL Training, Services, and 24x7 Support www.2ndQuadrant.com
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
btw, there is serious problem with row-level security and constraints. For
example, user with low security level could use unique constraint to know
about existence of a row with higher security. I don't know, what is the
best practice to avoid this.
On Wed, Aug 28, 2013 at 1:37 AM, Greg Smith <greg@2ndquadrant.com> wrote:
Show quoted text
On 7/20/13 10:08 AM, Kohei KaiGai wrote:
Hmm. I didn't have this idea. It seems to me fair enough and kills
necessity to enhance RangeTblEntry and getrelid() indeed.
I try to fix up this implementation according to your suggestion.How is that going? I'm going to do a serious review of this myself over
the next few weeks. I have a good chunk of time set aside for it as part
of a larger project. I'm hoping to get more people here involved in that
effort too, starting in the November CF if that works out.I've been trying to catch up with your larger plan for this feature for
9.4. You made this comment earlier:Also, I'd like to have discussion for this feature in earlier half of
v9.4 to keep time for the remaining features, such as check on
writer-side, integration with selinux, and so onIs any of that code around yet? I see that you have split your
submissions so that a smaller program can be reviewed today. I'd like to
start taking a look at the next step too though. For the project I'm
starting to work on here, getting the integration with labeling also done
is a very important thing to target for 9.4. It would be nice to see how
that fits together today, even if the code for it isn't being reviewed
heavily yet.I don't quite understand yet what's missing on the writer side. If you
could help explain what's missing there, I would like to read about that.--
Greg Smith 2ndQuadrant US greg@2ndQuadrant.com Baltimore, MD
PostgreSQL Training, Services, and 24x7 Support www.2ndQuadrant.com--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/**mailpref/pgsql-hackers<http://www.postgresql.org/mailpref/pgsql-hackers>
2013/8/27 Greg Smith <greg@2ndquadrant.com>:
On 7/20/13 10:08 AM, Kohei KaiGai wrote:
Hmm. I didn't have this idea. It seems to me fair enough and kills
necessity to enhance RangeTblEntry and getrelid() indeed.
I try to fix up this implementation according to your suggestion.How is that going? I'm going to do a serious review of this myself over the
next few weeks. I have a good chunk of time set aside for it as part of a
larger project. I'm hoping to get more people here involved in that effort
too, starting in the November CF if that works out.
Sorry, I tried to rework the portions that were not graceful so much, like
system column reference or update/delete on inherited tables, however,
it eventually came back to my original implementation. :-(
The attached patch fixed the portion I was pointed out, so its overall
design has not been changed so much.
I've been trying to catch up with your larger plan for this feature for 9.4.
You made this comment earlier:Also, I'd like to have discussion for this feature in earlier half of
v9.4 to keep time for the remaining features, such as check on
writer-side, integration with selinux, and so onIs any of that code around yet? I see that you have split your submissions
so that a smaller program can be reviewed today. I'd like to start taking a
look at the next step too though. For the project I'm starting to work on
here, getting the integration with labeling also done is a very important
thing to target for 9.4. It would be nice to see how that fits together
today, even if the code for it isn't being reviewed heavily yet.
The biggest (and most important) portion of overall picture is this patch;
that allows to restrict rows to be visible according to pre-configured
security policy per table.
Towards label based row-level security, it needs an infrastructure to
append before-row trigger on the fly, according to row-level security
configuration, because we need a check to prevent users to write
a record with unprivileged label. It is probably annoying requirement
for users to set up triggers for each table with security policy.
Then, I'd like to offer special functions of sepgsql that shall be used
to security policy function to filter out unprivileged tuples.
On the v9.4 time-frame, I'm not certain whether we can implement
facility to manage security label of user tables.
So, I assume label based row-level security is activated when
a special named text column is defined by users.
Towards v9.5, I'd like to have a feature to add hidden column for
security label purpose on table creation time without user's
consciousness.
I don't quite understand yet what's missing on the writer side. If you
could help explain what's missing there, I would like to read about that.
It needs to back the discussion at CF-4th of v9.3. We discussed whether
we should have a special feature to check records to be inserted or
newer records to be updated, because before-row trigger can offer its
infrastructure, even though it requires users an extra configuration job
for each tables that have row-level security policy.
What I'd like to add on writer-side is an infrastructure for extensions to
inject before-row trigger functions on the tail of call-chain, that allows
to prevent records with violated values, and also allows sepgsql to
assign a default security label on new records being inserted.
Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
Attachments:
pgsql-v9.4-row-level-security.v4.patchapplication/octet-stream; name=pgsql-v9.4-row-level-security.v4.patchDownload
doc/src/sgml/catalogs.sgml | 65 ++
doc/src/sgml/ref/alter_table.sgml | 43 ++
doc/src/sgml/user-manag.sgml | 145 +++++
src/backend/catalog/Makefile | 4 +-
src/backend/catalog/dependency.c | 8 +
src/backend/catalog/heap.c | 1 +
src/backend/catalog/objectaddress.c | 50 ++
src/backend/catalog/pg_rowsecurity.c | 337 ++++++++++
src/backend/commands/copy.c | 90 ++-
src/backend/commands/event_trigger.c | 1 +
src/backend/commands/tablecmds.c | 27 +
src/backend/executor/execMain.c | 27 +-
src/backend/nodes/copyfuncs.c | 3 +
src/backend/nodes/equalfuncs.c | 3 +
src/backend/nodes/nodeFuncs.c | 12 +-
src/backend/nodes/outfuncs.c | 3 +
src/backend/nodes/readfuncs.c | 2 +
src/backend/optimizer/plan/planner.c | 23 +-
src/backend/optimizer/prep/preptlist.c | 68 ++-
src/backend/optimizer/prep/prepunion.c | 95 ++-
src/backend/optimizer/util/Makefile | 2 +-
src/backend/optimizer/util/rowsecurity.c | 744 ++++++++++++++++++++++
src/backend/parser/gram.y | 25 +
src/backend/parser/parse_agg.c | 6 +
src/backend/parser/parse_expr.c | 3 +
src/backend/rewrite/rewriteHandler.c | 16 +
src/backend/utils/adt/ri_triggers.c | 13 +-
src/backend/utils/cache/plancache.c | 32 +
src/backend/utils/cache/relcache.c | 17 +-
src/bin/pg_dump/common.c | 4 +
src/bin/pg_dump/pg_backup_archiver.c | 1 +
src/bin/pg_dump/pg_dump.c | 186 +++++-
src/bin/pg_dump/pg_dump.h | 13 +-
src/bin/pg_dump/pg_dump_sort.c | 5 +
src/bin/psql/describe.c | 7 +
src/include/catalog/dependency.h | 1 +
src/include/catalog/indexing.h | 5 +
src/include/catalog/pg_class.h | 24 +-
src/include/catalog/pg_rowsecurity.h | 76 +++
src/include/miscadmin.h | 1 +
src/include/nodes/execnodes.h | 4 +
src/include/nodes/nodeFuncs.h | 1 +
src/include/nodes/parsenodes.h | 14 +-
src/include/nodes/plannodes.h | 2 +
src/include/nodes/relation.h | 6 +
src/include/optimizer/rowsecurity.h | 27 +
src/include/parser/parse_node.h | 3 +-
src/include/rewrite/rewriteHandler.h | 1 +
src/include/utils/plancache.h | 2 +
src/include/utils/rel.h | 2 +
src/test/regress/expected/rowsecurity.out | 950 +++++++++++++++++++++++++++++
src/test/regress/expected/sanity_check.out | 3 +-
src/test/regress/parallel_schedule | 2 +-
src/test/regress/serial_schedule | 1 +
src/test/regress/sql/rowsecurity.sql | 298 +++++++++
55 files changed, 3454 insertions(+), 50 deletions(-)
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 6715782..6f3c8fc 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -234,6 +234,11 @@
</row>
<row>
+ <entry><link linkend="catalog-pg-rowlevelsec"><structname>pg_rowlevelsec</structname></link></entry>
+ <entry>row-level security policy of relation</entry>
+ </row>
+
+ <row>
<entry><link linkend="catalog-pg-seclabel"><structname>pg_seclabel</structname></link></entry>
<entry>security labels on database objects</entry>
</row>
@@ -1848,6 +1853,16 @@
</row>
<row>
+ <entry><structfield>relhasrowsecurity</structfield></entry>
+ <entry><type>bool</type></entry>
+ <entry></entry>
+ <entry>
+ True if table has row-security policy; see
+ <link linkend="catalog-pg-rowsecurity"><structname>pg_rowsecurity</structname></link> catalog
+ </entry>
+ </row>
+
+ <row>
<entry><structfield>relhassubclass</structfield></entry>
<entry><type>bool</type></entry>
<entry></entry>
@@ -5112,6 +5127,56 @@
</sect1>
+ <sect1 id="catalog-pg-rowsecurity">
+ <title><structname>pg_rowlevelsec</structname></title>
+
+ <indexterm zone="catalog-pg-rowsecurity">
+ <primary>pg_rowsecurity</primary>
+ </indexterm>
+ <para>
+ The catalog <structname>pg_rowsecurity</structname> stores expression
+ tree of row-security policy to be performed on a particular relation.
+ </para>
+ <table>
+ <title><structname>pg_rowsecurity</structname> Columns</title>
+ <tgroup cols="4">
+ <thead>
+ <row>
+ <entry>Name</entry>
+ <entry>Type</entry>
+ <entry>References</entry>
+ <entry>Description</entry>
+ </row>
+ </thead>
+ <tbody>
+ <row>
+ <entry><structfield>rsecrelid</structfield></entry>
+ <entry><type>oid</type></entry>
+ <entry><literal><link linkend="catalog-pg-class"><structname>pg_class</structname></link>.oid</literal></entry>
+ <entry>The table this row-security is for</entry>
+ </row>
+ <row>
+ <entry><structfield>rseccmd</structfield></entry>
+ <entry><type>char</type></entry>
+ <entry></entry>
+ <entry>The command this row-security is for. 'a' meaning all is only possible value right now.</entry>
+ </row>
+ <row>
+ <entry><structfield>rsecqual</structfield></entry>
+ <entry><type>pg_node_tree</type></entry>
+ <entry></entry>
+ <entry>An expression tree to be performed as rowl-security policy</entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+ <note>
+ <para>
+ <literal>pg_class.relhasrowlevelsec</literal>
+ must be true if a table has row-level security policy in this catalog.
+ </para>
+ </note>
+ </sect1>
<sect1 id="catalog-pg-seclabel">
<title><structname>pg_seclabel</structname></title>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 2609d4a..b380852 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -69,12 +69,16 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable>
NOT OF
OWNER TO <replaceable class="PARAMETER">new_owner</replaceable>
SET TABLESPACE <replaceable class="PARAMETER">new_tablespace</replaceable>
+ SET ROW SECURITY FOR <replaceable class="PARAMETER">rowsec_command</replaceable> TO (<replaceable class="PARAMETER">condition</replaceable>)
+ RESET ROW SECURITY FOR <replaceable class="PARAMETER">rowsec_command</replaceable>
<phrase>and <replaceable class="PARAMETER">table_constraint_using_index</replaceable> is:</phrase>
[ CONSTRAINT <replaceable class="PARAMETER">constraint_name</replaceable> ]
{ UNIQUE | PRIMARY KEY } USING INDEX <replaceable class="PARAMETER">index_name</replaceable>
[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+<phrase>and <replaceable class="PARAMETER">rowsec_command</replaceable> is:</phrase>
+ { ALL | SELECT | INSERT | UPDATE | DELETE }
</synopsis>
</refsynopsisdiv>
@@ -580,6 +584,31 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable>
</varlistentry>
<varlistentry>
+ <term><literal>SET ROW SECURITY FOR <replaceable class="PARAMETER">rowsec_command</replaceable> TO (<replaceable class="PARAMETER">condition</replaceable>)</literal></term>
+ <listitem>
+ <para>
+ This form set row-level security policy of the table.
+ Supplied <replaceable class="PARAMETER">condition</replaceable> performs
+ as if it is implicitly appended to the qualifiers of <literal>WHERE</literal>
+ clause, although mechanism guarantees to evaluate this condition earlier
+ than any other user given condition.
+ <literal>ALL</> is the only supported command type right now.
+ See also <xref linkend="ROW-SECURITY">.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>RESET ROW SECURITY FOR <replaceable class="PARAMETER">rowsec_command</replaceable></literal></term>
+ <listitem>
+ <para>
+ This form reset row-level security policy of the table, if exists.
+ <literal>ALL</> is the only supported command type right now.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
<term><literal>RENAME</literal></term>
<listitem>
<para>
@@ -821,6 +850,20 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable>
</listitem>
</varlistentry>
+ <varlistentry>
+ <term><replaceable class="PARAMETER">condition</replaceable></term>
+ <listitem>
+ <para>
+ An expression that returns a value of type boolean. Expect for a case
+ when queries are executed with superuser privilege, only rows for which
+ this expression returns true will be fetched, updated or deleted.
+ This expression can reference columns of the relation being configured.
+ Sub-queries can be contained within expression tree, unless referenced
+ relation recursively references the same relation.
+ </para>
+ </listitem>
+ </varlistentry>
+
</variablelist>
</refsect1>
diff --git a/doc/src/sgml/user-manag.sgml b/doc/src/sgml/user-manag.sgml
index 177ac7a..c764189 100644
--- a/doc/src/sgml/user-manag.sgml
+++ b/doc/src/sgml/user-manag.sgml
@@ -439,4 +439,149 @@ DROP ROLE <replaceable>name</replaceable>;
</para>
</sect1>
+ <sect1 id="row-security">
+ <title>Row-security</title>
+ <para>
+ <productname>PostgreSQL</productname> v9.4 or later provides
+ row-security feature, like several commercial database
+ management system. It allows table owner to assign a particular
+ condition that performs as a security policy of the table; only
+ rows that satisfies the condition should be visible, except for
+ a case when superuser runs queries.
+ </para>
+ <para>
+ Row-security policy can be set using <literal>SET ROW SECURITY</literal>
+ command of <xref linkend="SQL-ALTERTABLE"> 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.
+ </para>
+ <para>
+ For example, the following <literal>customer</literal> table has
+ <literal>uname</literal> field to store user name, and it assume
+ we don't want to expose any properties of other customers.
+ The following command set <literal>current_user = uname</literal>
+ as row-security policy on the <literal>customer</literal> table.
+<screen>
+postgres=> ALTER TABLE customer SET ROW SECURITY
+ FOR ALL TO (current_user = uname);
+ALTER TABLE
+</screen>
+ <xref linkend="SQL-EXPLAIN"> command shows how row-security policy
+ works on the supplied query.
+<screen>
+postgres=> EXPLAIN(costs off) SELECT * FROM customer WHERE f_leak(upasswd);
+ QUERY PLAN
+--------------------------------------------
+ Subquery Scan on customer
+ Filter: f_leak(customer.upasswd)
+ -> Seq Scan on customer customer_1
+ Filter: ("current_user"() = uname)
+(4 rows)
+</screen>
+ This query execution plan means the preconfigured row-security policy is
+ implicitly added, and scan plan on the target relation being wrapped up
+ with a sub-query.
+ It ensures user given qualifiers, including functions with side effects,
+ are never executed earlier than the row-security policy regardless of
+ its cost, except for the cases when these were fully leakproof.
+ This design helps to tackle the scenario described in
+ <xref linkend="RULES-PRIVILEGES">; that introduces the order to evaluate
+ qualifiers is significant to keep confidentiality of invisible rows.
+ </para>
+
+ <para>
+ On the other hand, this design allows superusers to bypass checks with
+ row-security.
+ It ensures <application>pg_dump</application> can obtain a complete set
+ of database backup, and avoid to execute Trojan horse trap, being injected
+ as a row-security policy of user-defined table, with privileges of
+ superuser.
+ </para>
+
+ <para>
+ In case of queries on inherited tables, row-security policy of the parent
+ relation is not applied to child relations. Scope of the row-security
+ policy is limited to the relation on which it is set.
+<screen>
+postgres=> EXPLAIN(costs off) SELECT * FROM t1 WHERE f_leak(y);
+ QUERY PLAN
+-------------------------------------
+ Append
+ -> Subquery Scan on t1
+ Filter: f_leak(t1.y)
+ -> Seq Scan on t1 t1_1
+ Filter: ((x % 2) = 0)
+ -> Seq Scan on t2
+ Filter: f_leak(y)
+ -> Subquery Scan on t3
+ Filter: f_leak(t3.y)
+ -> Seq Scan on t3 t3_1
+ Filter: ((x % 2) = 1)
+(11 rows)
+</screen>
+ In the above example, <literal>t1</literal> has inherited
+ child table <literal>t2</literal> and <literal>t3</literal>,
+ and row-security policy is set on only <literal>t1</literal>,
+ and <literal>t3</literal>, not <literal>t2</literal>.
+
+ The row-security policy of <literal>t1</literal>, <literal>x</literal>
+ must be even-number, is appended only <literal>t1</literal>,
+ neither <literal>t2</literal> nor <literal>t3</literal>.
+ On the contrary, <literal>t3</literal> has different row-security policy;
+ <literal>x</literal> must be odd-number.
+ </para>
+
+ <para>
+ Row-security feature also works to queries for writer-operations; such as
+ <xref linkend="SQL-INSERT">, <xref linkend="SQL-UPDATE"> or
+ <xref linkend="SQL-DELETE"> commands.
+ It ensures all the modified rows satisfies configured row-security policy.
+ The below query tries to update e-mail address of the
+ <literal>customer</> table, and configured row-security makes sure all the
+ modified rows's <literal>uname</> field has to match with
+ <literal>current_user</>.
+ </para>
+<screen>
+postgres=> EXPLAIN (costs off) UPDATE customer SET email = 'alice@example.com';
+ QUERY PLAN
+--------------------------------------------------
+ Update on customer
+ -> Subquery Scan on customer
+ -> Seq Scan on customer customer_1
+ Filter: ("current_user"() = uname)
+(4 rows)
+</screen>
+ <para>
+ Modification of a certain table is consist of two different stuffs; one
+ is fetch rows to be modified from the result relation (except for
+ <literal>INSERT</> command), second is insertion of rows to the
+ result relation. Usually, before-row trigger or check constraints are
+ run in the second phase. In addition, row-security policy is also checked
+ on this stage, to prevent to insert or update rows with unprivileged
+ values.
+ </para>
+
+ <para>
+ Unlike other commercial database systems, we don't have any plan to allow
+ individual row-security policy for each command type. Even if we want to
+ perform with difference policy between <xref linkend="SQL-SELECT"> and
+ <xref linkend="SQL-DELETE">, <literal>RETURNING</> clause can leak the rows
+ to be invisible using <xref linkend="SQL-DELETE"> command.
+ </para>
+
+ <para>
+ Even though it is not a specific matter in row-security, please be careful
+ if you plan to use <literal>current_user</> in row-level security policy.
+ <xref linkend="SQL-DECLARE"> and <xref linkend="SQL-FETCH"> allows to
+ switch current user identifier during execution of the query.
+ Thus, it can fetch the first 100 rows with privilege of <literal>alice</>,
+ then remaining rows with privilege of <literal>bob</>. If and when query
+ execution plan contains some kind of materialization and row-security
+ policy contains <literal>current_user</>, the fetched tuples in
+ <literal>bob</>'s screen might be evaluated according to the privilege of
+ <literal>alice</>.
+ </para>
+ </sect1>
</chapter>
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index c4d3f3c..9b4e9f5 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -15,7 +15,7 @@ OBJS = catalog.o dependency.o heap.o index.o indexing.o namespace.o aclchk.o \
pg_constraint.o pg_conversion.o \
pg_depend.o pg_enum.o pg_inherits.o pg_largeobject.o pg_namespace.o \
pg_operator.o pg_proc.o pg_range.o pg_db_role_setting.o pg_shdepend.o \
- pg_type.o storage.o toasting.o
+ pg_rowsecurity.o pg_type.o storage.o toasting.o
BKIFILES = postgres.bki postgres.description postgres.shdescription
@@ -39,7 +39,7 @@ POSTGRES_BKI_SRCS = $(addprefix $(top_srcdir)/src/include/catalog/,\
pg_ts_config.h pg_ts_config_map.h pg_ts_dict.h \
pg_ts_parser.h pg_ts_template.h pg_extension.h \
pg_foreign_data_wrapper.h pg_foreign_server.h pg_user_mapping.h \
- pg_foreign_table.h \
+ pg_foreign_table.h pg_rowsecurity.h \
pg_default_acl.h pg_seclabel.h pg_shseclabel.h pg_collation.h pg_range.h \
toasting.h indexing.h \
)
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index fe17c96..d13b2e1 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_rowsecurity.h"
#include "catalog/pg_tablespace.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_ts_config.h"
@@ -1249,6 +1250,10 @@ doDeletion(const ObjectAddress *object, int flags)
RemoveEventTriggerById(object->objectId);
break;
+ case OCLASS_ROWSECURITY:
+ RemoveRowSecurityById(object->objectId);
+ break;
+
default:
elog(ERROR, "unrecognized object class: %u",
object->classId);
@@ -2308,6 +2313,9 @@ getObjectClass(const ObjectAddress *object)
case EventTriggerRelationId:
return OCLASS_EVENT_TRIGGER;
+
+ case RowSecurityRelationId:
+ return OCLASS_ROWSECURITY;
}
/* shouldn't get here */
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 64ca312..7eb7a17 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -791,6 +791,7 @@ InsertPgClassTuple(Relation pg_class_desc,
values[Anum_pg_class_relhaspkey - 1] = BoolGetDatum(rd_rel->relhaspkey);
values[Anum_pg_class_relhasrules - 1] = BoolGetDatum(rd_rel->relhasrules);
values[Anum_pg_class_relhastriggers - 1] = BoolGetDatum(rd_rel->relhastriggers);
+ values[Anum_pg_class_relhasrowsecurity - 1] = BoolGetDatum(rd_rel->relhasrowsecurity);
values[Anum_pg_class_relhassubclass - 1] = BoolGetDatum(rd_rel->relhassubclass);
values[Anum_pg_class_relispopulated - 1] = BoolGetDatum(rd_rel->relispopulated);
values[Anum_pg_class_relfrozenxid - 1] = TransactionIdGetDatum(rd_rel->relfrozenxid);
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 4d22f3a..b2aac6a 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -2143,6 +2143,56 @@ getObjectDescription(const ObjectAddress *object)
break;
}
+ case OCLASS_ROWSECURITY:
+ {
+ Relation rsec_rel;
+ ScanKeyData skey;
+ SysScanDesc sscan;
+ HeapTuple tuple;
+ Form_pg_rowsecurity form_rsec;
+
+ rsec_rel = heap_open(RowSecurityRelationId, AccessShareLock);
+
+ ScanKeyInit(&skey,
+ ObjectIdAttributeNumber,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(object->objectId));
+ sscan = systable_beginscan(rsec_rel, RowSecurityOidIndexId,
+ true, NULL, 1, &skey);
+ tuple = systable_getnext(sscan);
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "could not find tuple for row-security %u",
+ object->objectId);
+ form_rsec = (Form_pg_rowsecurity) GETSTRUCT(tuple);
+
+ appendStringInfo(&buffer, _("row-security of "));
+ getRelationDescription(&buffer, form_rsec->rsecrelid);
+ switch (form_rsec->rseccmd)
+ {
+ case ROWSECURITY_CMD_ALL:
+ appendStringInfo(&buffer, _(" FOR ALL"));
+ break;
+ case ROWSECURITY_CMD_SELECT:
+ appendStringInfo(&buffer, _(" FOR SELECT"));
+ break;
+ case ROWSECURITY_CMD_INSERT:
+ appendStringInfo(&buffer, _(" FOR INSERT"));
+ break;
+ case ROWSECURITY_CMD_UPDATE:
+ appendStringInfo(&buffer, _(" FOR UPDATE"));
+ break;
+ case ROWSECURITY_CMD_DELETE:
+ appendStringInfo(&buffer, _(" FOR DELETE"));
+ break;
+ default:
+ elog(ERROR, "unexpected row-security command type: %c",
+ form_rsec->rseccmd);
+ }
+ systable_endscan(sscan);
+ heap_close(rsec_rel, AccessShareLock);
+ break;
+ }
+
default:
appendStringInfo(&buffer, "unrecognized object %u %u %d",
object->classId,
diff --git a/src/backend/catalog/pg_rowsecurity.c b/src/backend/catalog/pg_rowsecurity.c
new file mode 100644
index 0000000..34d33e8
--- /dev/null
+++ b/src/backend/catalog/pg_rowsecurity.c
@@ -0,0 +1,337 @@
+/* -------------------------------------------------------------------------
+ *
+ * pg_rowsecurity.c
+ * routines to support manipulation of the pg_rowsecurity catalog
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * -------------------------------------------------------------------------
+ */
+#include "postgres.h"
+#include "access/genam.h"
+#include "access/heapam.h"
+#include "access/htup_details.h"
+#include "access/sysattr.h"
+#include "catalog/dependency.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_rowsecurity.h"
+#include "catalog/pg_type.h"
+#include "nodes/nodeFuncs.h"
+#include "optimizer/clauses.h"
+#include "parser/parse_clause.h"
+#include "parser/parse_node.h"
+#include "parser/parse_relation.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/inval.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/tqual.h"
+
+/*
+ * Load row-security policy from the catalog, and keep it on
+ * the relation cache.
+ */
+void
+RelationBuildRowSecurity(Relation relation)
+{
+ Relation catalog;
+ ScanKeyData skey;
+ SysScanDesc sscan;
+ HeapTuple tuple;
+ MemoryContext oldcxt;
+ MemoryContext rscxt = NULL;
+ RowSecurityDesc *rsdesc = NULL;
+
+ catalog = heap_open(RowSecurityRelationId, AccessShareLock);
+
+ ScanKeyInit(&skey,
+ Anum_pg_rowsecurity_rsecrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(RelationGetRelid(relation)));
+ sscan = systable_beginscan(catalog, RowSecurityRelidIndexId, true,
+ NULL, 1, &skey);
+ PG_TRY();
+ {
+ while (HeapTupleIsValid(tuple = systable_getnext(sscan)))
+ {
+ Datum value;
+ bool isnull;
+ char *temp;
+
+ if (!rsdesc)
+ {
+ rscxt = AllocSetContextCreate(CacheMemoryContext,
+ "Row-security descriptor",
+ ALLOCSET_SMALL_MINSIZE,
+ ALLOCSET_SMALL_INITSIZE,
+ ALLOCSET_SMALL_MAXSIZE);
+ oldcxt = MemoryContextSwitchTo(rscxt);
+ rsdesc = palloc0(sizeof(RowSecurityDesc));
+ rsdesc->rscxt = rscxt;
+ MemoryContextSwitchTo(oldcxt);
+ }
+ value = heap_getattr(tuple, Anum_pg_rowsecurity_rseccmd,
+ RelationGetDescr(catalog), &isnull);
+ Assert(!isnull);
+
+ if (DatumGetChar(value) != ROWSECURITY_CMD_ALL)
+ {
+ ereport(WARNING,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("Per-command row-security not implemented")));
+ continue;
+ }
+
+ value = heap_getattr(tuple, Anum_pg_rowsecurity_rsecqual,
+ RelationGetDescr(catalog), &isnull);
+ Assert(!isnull);
+ temp = TextDatumGetCString(value);
+
+ oldcxt = MemoryContextSwitchTo(rscxt);
+ rsdesc->rsall.rsecid = HeapTupleGetOid(tuple);
+ rsdesc->rsall.qual = (Expr *) stringToNode(temp);
+ Assert(exprType((Node *)rsdesc->rsall.qual) == BOOLOID);
+ rsdesc->rsall.hassublinks
+ = contain_subplans((Node *)rsdesc->rsall.qual);
+ MemoryContextSwitchTo(oldcxt);
+
+ pfree(temp);
+ }
+ }
+ PG_CATCH();
+ {
+ if (rscxt != NULL)
+ MemoryContextDelete(rscxt);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ systable_endscan(sscan);
+ heap_close(catalog, AccessShareLock);
+
+ relation->rsdesc = rsdesc;
+}
+
+/*
+ * Parse the supplied row-security policy, and insert/update a row
+ * of pg_rowsecurity catalog.
+ */
+static void
+InsertOrUpdatePolicyRow(Relation relation, char rseccmd, Node *clause)
+{
+ Oid relationId = RelationGetRelid(relation);
+ Oid rowsecId;
+ ParseState *pstate;
+ RangeTblEntry *rte;
+ Node *qual;
+ Relation catalog;
+ ScanKeyData skeys[2];
+ SysScanDesc sscan;
+ HeapTuple oldtup;
+ HeapTuple newtup;
+ Datum values[Natts_pg_rowsecurity];
+ bool isnull[Natts_pg_rowsecurity];
+ bool replaces[Natts_pg_rowsecurity];
+ ObjectAddress target;
+ ObjectAddress myself;
+
+ /* Parse the supplied clause */
+ pstate = make_parsestate(NULL);
+
+ rte = addRangeTableEntryForRelation(pstate, relation,
+ NULL, false, false);
+ addRTEtoQuery(pstate, rte, false, true, true);
+
+ qual = transformWhereClause(pstate, copyObject(clause),
+ EXPR_KIND_ROW_SECURITY,
+ "ROW SECURITY");
+ /* zero-clear */
+ memset(values, 0, sizeof(values));
+ memset(replaces, 0, sizeof(replaces));
+ memset(isnull, 0, sizeof(isnull));
+
+ /* Update or Insert an entry to pg_rowsecurity catalog */
+ catalog = heap_open(RowSecurityRelationId, RowExclusiveLock);
+
+ ScanKeyInit(&skeys[0],
+ Anum_pg_rowsecurity_rsecrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(RelationGetRelid(relation)));
+ ScanKeyInit(&skeys[1],
+ Anum_pg_rowsecurity_rseccmd,
+ BTEqualStrategyNumber, F_CHAREQ,
+ CharGetDatum(rseccmd));
+ sscan = systable_beginscan(catalog, RowSecurityRelidIndexId, true,
+ NULL, 2, skeys);
+ oldtup = systable_getnext(sscan);
+ if (HeapTupleIsValid(oldtup))
+ {
+ rowsecId = HeapTupleGetOid(oldtup);
+
+ replaces[Anum_pg_rowsecurity_rsecqual - 1] = true;
+ values[Anum_pg_rowsecurity_rsecqual - 1]
+ = CStringGetTextDatum(nodeToString(qual));
+
+ newtup = heap_modify_tuple(oldtup,
+ RelationGetDescr(catalog),
+ values, isnull, replaces);
+ simple_heap_update(catalog, &newtup->t_self, newtup);
+
+ deleteDependencyRecordsFor(RowSecurityRelationId, rowsecId, false);
+ }
+ else
+ {
+ values[Anum_pg_rowsecurity_rsecrelid - 1]
+ = ObjectIdGetDatum(relationId);
+ values[Anum_pg_rowsecurity_rseccmd - 1]
+ = CharGetDatum(rseccmd);
+ values[Anum_pg_rowsecurity_rsecqual - 1]
+ = CStringGetTextDatum(nodeToString(qual));
+ newtup = heap_form_tuple(RelationGetDescr(catalog),
+ values, isnull);
+ rowsecId = simple_heap_insert(catalog, newtup);
+ }
+ CatalogUpdateIndexes(catalog, newtup);
+
+ heap_freetuple(newtup);
+
+ /* records dependencies of row-security policy and relation/columns */
+ target.classId = RelationRelationId;
+ target.objectId = relationId;
+ target.objectSubId = 0;
+
+ myself.classId = RowSecurityRelationId;
+ myself.objectId = rowsecId;
+ myself.objectSubId = 0;
+
+ recordDependencyOn(&myself, &target, DEPENDENCY_AUTO);
+
+ recordDependencyOnExpr(&myself, qual, pstate->p_rtable,
+ DEPENDENCY_NORMAL);
+ free_parsestate(pstate);
+
+ systable_endscan(sscan);
+ heap_close(catalog, RowExclusiveLock);
+}
+
+/*
+ * Remove row-security policy row of pg_rowsecurity
+ */
+static void
+DeletePolicyRow(Relation relation, char rseccmd)
+{
+ Assert(rseccmd == ROWSECURITY_CMD_ALL);
+
+ if (relation->rsdesc)
+ {
+ ObjectAddress address;
+
+ address.classId = RowSecurityRelationId;
+ address.objectId = relation->rsdesc->rsall.rsecid;
+ address.objectSubId = 0;
+
+ performDeletion(&address, DROP_RESTRICT, 0);
+ }
+ else
+ {
+ /* Nothing to do here */
+ elog(INFO, "relation %s has no row-security policy, skipped",
+ RelationGetRelationName(relation));
+ }
+}
+
+/*
+ * Guts of row-security policy deletion.
+ */
+void
+RemoveRowSecurityById(Oid rowsecId)
+{
+ Relation catalog;
+ ScanKeyData skey;
+ SysScanDesc sscan;
+ HeapTuple tuple;
+ Relation rel;
+ Oid relid;
+
+ catalog = heap_open(RowSecurityRelationId, RowExclusiveLock);
+
+ ScanKeyInit(&skey,
+ ObjectIdAttributeNumber,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(rowsecId));
+ sscan = systable_beginscan(catalog, RowSecurityOidIndexId, true,
+ NULL, 1, &skey);
+ tuple = systable_getnext(sscan);
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "could not find tuple for row-security %u", rowsecId);
+
+ /*
+ * Open and exclusive-lock the relation the row-security belongs to.
+ */
+ relid = ((Form_pg_rowsecurity) GETSTRUCT(tuple))->rsecrelid;
+
+ rel = heap_open(relid, AccessExclusiveLock);
+
+ simple_heap_delete(catalog, &tuple->t_self);
+
+ /* Ensure relcache entries of other session being rebuilt */
+ CacheInvalidateRelcache(rel);
+
+ heap_close(rel, NoLock);
+
+ systable_endscan(sscan);
+ heap_close(catalog, RowExclusiveLock);
+}
+
+/*
+ * ALTER TABLE <name> SET ROW SECURITY (...) OR
+ * RESET ROW SECURITY
+ */
+void
+ATExecSetRowSecurity(Relation relation, const char *cmdname, Node *clause)
+{
+ Oid relid = RelationGetRelid(relation);
+ char rseccmd;
+
+ if (strcmp(cmdname, "all") == 0)
+ rseccmd = ROWSECURITY_CMD_ALL;
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("Row-security for \"%s\" is not implemented yet",
+ cmdname)));
+
+ if (clause != NULL)
+ {
+ InsertOrUpdatePolicyRow(relation, rseccmd, clause);
+
+ /*
+ * Also, turn on relhasrowsecurity, if not.
+ */
+ if (!RelationGetForm(relation)->relhasrowsecurity)
+ {
+ Relation class_rel = heap_open(RelationRelationId,
+ RowExclusiveLock);
+ HeapTuple tuple;
+
+ tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for relation %u", relid);
+
+ ((Form_pg_class) GETSTRUCT(tuple))->relhasrowsecurity = true;
+
+ simple_heap_update(class_rel, &tuple->t_self, tuple);
+ CatalogUpdateIndexes(class_rel, tuple);
+
+ heap_freetuple(tuple);
+ heap_close(class_rel, RowExclusiveLock);
+ }
+ }
+ else
+ DeletePolicyRow(relation, rseccmd);
+
+ CacheInvalidateRelcache(relation);
+}
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 31819cc..9cb8f1b 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -24,6 +24,7 @@
#include "access/htup_details.h"
#include "access/sysattr.h"
#include "access/xact.h"
+#include "catalog/heap.h"
#include "catalog/namespace.h"
#include "catalog/pg_type.h"
#include "commands/copy.h"
@@ -34,15 +35,19 @@
#include "libpq/pqformat.h"
#include "mb/pg_wchar.h"
#include "miscadmin.h"
+#include "nodes/makefuncs.h"
#include "optimizer/clauses.h"
#include "optimizer/planner.h"
+#include "optimizer/rowsecurity.h"
#include "parser/parse_relation.h"
+#include "parser/parsetree.h"
#include "rewrite/rewriteHandler.h"
#include "storage/fd.h"
#include "tcop/tcopprot.h"
#include "utils/acl.h"
#include "utils/builtins.h"
#include "utils/lsyscache.h"
+#include "utils/syscache.h"
#include "utils/memutils.h"
#include "utils/portal.h"
#include "utils/rel.h"
@@ -814,6 +819,21 @@ DoCopy(const CopyStmt *stmt, const char *queryString, uint64 *processed)
rel = heap_openrv(stmt->relation,
(is_from ? RowExclusiveLock : AccessShareLock));
+ tupDesc = RelationGetDescr(rel);
+ attnums = CopyGetAttnums(tupDesc, rel, stmt->attlist);
+
+ /*
+ * We have to run regular query, if the target relation has
+ * row-level security policy
+ */
+ if (copy_row_security_policy((CopyStmt *)stmt, rel, attnums))
+ {
+ heap_close(rel, NoLock); /* close with keeping lock */
+ relid = InvalidOid;
+ rel = NULL;
+ }
+ else
+ {
relid = RelationGetRelid(rel);
rte = makeNode(RangeTblEntry);
@@ -822,8 +842,6 @@ DoCopy(const CopyStmt *stmt, const char *queryString, uint64 *processed)
rte->relkind = rel->rd_rel->relkind;
rte->requiredPerms = required_access;
- tupDesc = RelationGetDescr(rel);
- attnums = CopyGetAttnums(tupDesc, rel, stmt->attlist);
foreach(cur, attnums)
{
int attno = lfirst_int(cur) -
@@ -835,6 +853,7 @@ DoCopy(const CopyStmt *stmt, const char *queryString, uint64 *processed)
rte->selectedCols = bms_add_member(rte->selectedCols, attno);
}
ExecCheckRTPerms(list_make1(rte), true);
+ }
}
else
{
@@ -1193,6 +1212,53 @@ ProcessCopyOptions(CopyState cstate,
}
/*
+ * Adjust Query tree constructed with row-level security feature.
+ * If WITH OIDS option was supplied, it adds Var node to reference
+ * object-id system column.
+ */
+static void
+fixup_oid_of_rls_query(Query *query)
+{
+ RangeTblEntry *subrte;
+ TargetEntry *subtle;
+ Var *subvar;
+ ListCell *cell;
+ Form_pg_attribute attform
+ = SystemAttributeDefinition(ObjectIdAttributeNumber, true);
+
+ subrte = rt_fetch((Index) 1, query->rtable);
+ Assert(subrte->rtekind == RTE_RELATION);
+
+ if (!SearchSysCacheExists2(ATTNUM,
+ ObjectIdGetDatum(subrte->relid),
+ Int16GetDatum(attform->attnum)))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_COLUMN),
+ errmsg("table \"%s\" does not have OIDs",
+ get_rel_name(subrte->relid))));
+
+ subvar = makeVar((Index) 1,
+ attform->attnum,
+ attform->atttypid,
+ attform->atttypmod,
+ attform->attcollation,
+ 0);
+ subtle = makeTargetEntry((Expr *) subvar,
+ 0,
+ pstrdup(NameStr(attform->attname)),
+ false);
+
+ query->targetList = list_concat(list_make1(subtle),
+ query->targetList);
+ /* adjust resno of TargetEntry */
+ foreach (cell, query->targetList)
+ {
+ subtle = lfirst(cell);
+ subtle->resno++;
+ }
+}
+
+/*
* Common setup routines used by BeginCopyFrom and BeginCopyTo.
*
* Iff <binary>, unload or reload in the binary format, as opposed to the
@@ -1264,6 +1330,25 @@ BeginCopy(bool is_from,
Assert(!is_from);
cstate->rel = NULL;
+ /*
+ * In case when regular COPY TO was replaced because of row-level
+ * security, "raw_query" node have already analyzed / rewritten
+ * query tree.
+ */
+ if (IsA(raw_query, Query))
+ {
+ query = (Query *) raw_query;
+
+ Assert(query->querySource == QSRC_ROW_SECURITY);
+ if (cstate->oids)
+ {
+ fixup_oid_of_rls_query(query);
+ cstate->oids = false;
+ }
+ attnamelist = NIL;
+ }
+ else
+ {
/* Don't allow COPY w/ OIDs from a select */
if (cstate->oids)
ereport(ERROR,
@@ -1288,6 +1373,7 @@ BeginCopy(bool is_from,
elog(ERROR, "unexpected rewrite result");
query = (Query *) linitial(rewritten);
+ }
/* The grammar allows SELECT INTO, but we don't support that */
if (query->utilityStmt != NULL &&
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 328e2a8..0374629 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -992,6 +992,7 @@ EventTriggerSupportsObjectClass(ObjectClass objclass)
case OCLASS_USER_MAPPING:
case OCLASS_DEFACL:
case OCLASS_EXTENSION:
+ case OCLASS_ROWSECURITY:
return true;
case MAX_OCLASS:
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index adc74dd..0f9efa0 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -37,6 +37,7 @@
#include "catalog/pg_inherits_fn.h"
#include "catalog/pg_namespace.h"
#include "catalog/pg_opclass.h"
+#include "catalog/pg_rowsecurity.h"
#include "catalog/pg_tablespace.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
@@ -2787,6 +2788,8 @@ AlterTableGetLockLevel(List *cmds)
case AT_SetTableSpace: /* must rewrite heap */
case AT_DropNotNull: /* may change some SQL plans */
case AT_SetNotNull:
+ case AT_SetRowSecurity:
+ case AT_ResetRowSecurity:
case AT_GenericOptions:
case AT_AlterColumnGenericOptions:
cmd_lockmode = AccessExclusiveLock;
@@ -3155,6 +3158,8 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
case AT_DropInherit: /* NO INHERIT */
case AT_AddOf: /* OF */
case AT_DropOf: /* NOT OF */
+ case AT_SetRowSecurity:
+ case AT_ResetRowSecurity:
ATSimplePermissions(rel, ATT_TABLE);
/* These commands never recurse */
/* No command-specific prep needed */
@@ -3440,6 +3445,12 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab, Relation rel,
case AT_DropOf:
ATExecDropOf(rel, lockmode);
break;
+ case AT_SetRowSecurity:
+ ATExecSetRowSecurity(rel, cmd->name, (Node *) cmd->def);
+ break;
+ case AT_ResetRowSecurity:
+ ATExecSetRowSecurity(rel, cmd->name, NULL);
+ break;
case AT_GenericOptions:
ATExecGenericOptions(rel, (List *) cmd->def);
break;
@@ -7789,6 +7800,22 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
Assert(defaultexpr);
break;
+ case OCLASS_ROWSECURITY:
+ /*
+ * A row-level security policy can depend on a column in case
+ * when the policy clause references a particular column.
+ * Due to same reason why TRIGGER ... WHEN does not support
+ * to change column's type being referenced in clause, row-
+ * level security policy also does not support it.
+ */
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot alter type of a column used in a row-level security policy"),
+ errdetail("%s depends on column \"%s\"",
+ getObjectDescription(&foundObject),
+ colName)));
+ break;
+
case OCLASS_PROC:
case OCLASS_TYPE:
case OCLASS_CAST:
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 791f336..619710d 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -788,8 +788,9 @@ InitPlan(QueryDesc *queryDesc, int eflags)
foreach(l, plannedstmt->rowMarks)
{
PlanRowMark *rc = (PlanRowMark *) lfirst(l);
- Oid relid;
- Relation relation;
+ RangeTblEntry *rte = NULL;
+ Relation relation = NULL;
+ LOCKMODE lockmode = NoLock;
ExecRowMark *erm;
/* ignore "parent" rowmarks; they are irrelevant at runtime */
@@ -802,27 +803,33 @@ InitPlan(QueryDesc *queryDesc, int eflags)
case ROW_MARK_NOKEYEXCLUSIVE:
case ROW_MARK_SHARE:
case ROW_MARK_KEYSHARE:
- relid = getrelid(rc->rti, rangeTable);
- relation = heap_open(relid, RowShareLock);
+ rte = rt_fetch(rc->rti, rangeTable);
+ lockmode = RowShareLock;
break;
case ROW_MARK_REFERENCE:
- relid = getrelid(rc->rti, rangeTable);
- relation = heap_open(relid, AccessShareLock);
+ rte = rt_fetch(rc->rti, rangeTable);
+ lockmode = AccessShareLock;
break;
case ROW_MARK_COPY:
/* there's no real table here ... */
- relation = NULL;
break;
default:
elog(ERROR, "unrecognized markType: %d", rc->markType);
- relation = NULL; /* keep compiler quiet */
break;
}
/* Check that relation is a legal target for marking */
- if (relation)
+ if (rte)
+ {
+ if (rte->rtekind == RTE_RELATION)
+ relation = heap_open(rte->relid, lockmode);
+ else
+ {
+ Assert(rte->rtekind == RTE_SUBQUERY);
+ relation = heap_open(rte->rowsec_relid, lockmode);
+ }
CheckValidRowMarkRel(relation, rc->markType);
-
+ }
erm = (ExecRowMark *) palloc(sizeof(ExecRowMark));
erm->relation = relation;
erm->rti = rc->rti;
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 788907e..a9c072c 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -1937,6 +1937,7 @@ _copyAppendRelInfo(const AppendRelInfo *from)
COPY_SCALAR_FIELD(parent_relid);
COPY_SCALAR_FIELD(child_relid);
+ COPY_SCALAR_FIELD(child_result);
COPY_SCALAR_FIELD(parent_reltype);
COPY_SCALAR_FIELD(child_reltype);
COPY_NODE_FIELD(translated_vars);
@@ -1978,6 +1979,7 @@ _copyRangeTblEntry(const RangeTblEntry *from)
COPY_SCALAR_FIELD(relkind);
COPY_NODE_FIELD(subquery);
COPY_SCALAR_FIELD(security_barrier);
+ COPY_SCALAR_FIELD(rowsec_relid);
COPY_SCALAR_FIELD(jointype);
COPY_NODE_FIELD(joinaliasvars);
COPY_NODE_FIELD(funcexpr);
@@ -2451,6 +2453,7 @@ _copyQuery(const Query *from)
COPY_SCALAR_FIELD(canSetTag);
COPY_NODE_FIELD(utilityStmt);
COPY_SCALAR_FIELD(resultRelation);
+ COPY_SCALAR_FIELD(sourceRelation);
COPY_SCALAR_FIELD(hasAggs);
COPY_SCALAR_FIELD(hasWindowFuncs);
COPY_SCALAR_FIELD(hasSubLinks);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 496e31d..ee69fdc 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -811,6 +811,7 @@ _equalAppendRelInfo(const AppendRelInfo *a, const AppendRelInfo *b)
{
COMPARE_SCALAR_FIELD(parent_relid);
COMPARE_SCALAR_FIELD(child_relid);
+ COMPARE_SCALAR_FIELD(child_result);
COMPARE_SCALAR_FIELD(parent_reltype);
COMPARE_SCALAR_FIELD(child_reltype);
COMPARE_NODE_FIELD(translated_vars);
@@ -846,6 +847,7 @@ _equalQuery(const Query *a, const Query *b)
COMPARE_SCALAR_FIELD(canSetTag);
COMPARE_NODE_FIELD(utilityStmt);
COMPARE_SCALAR_FIELD(resultRelation);
+ COMPARE_SCALAR_FIELD(sourceRelation);
COMPARE_SCALAR_FIELD(hasAggs);
COMPARE_SCALAR_FIELD(hasWindowFuncs);
COMPARE_SCALAR_FIELD(hasSubLinks);
@@ -2233,6 +2235,7 @@ _equalRangeTblEntry(const RangeTblEntry *a, const RangeTblEntry *b)
COMPARE_SCALAR_FIELD(relkind);
COMPARE_NODE_FIELD(subquery);
COMPARE_SCALAR_FIELD(security_barrier);
+ COMPARE_SCALAR_FIELD(rowsec_relid);
COMPARE_SCALAR_FIELD(jointype);
COMPARE_NODE_FIELD(joinaliasvars);
COMPARE_NODE_FIELD(funcexpr);
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 908f397..88ae823 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -1936,8 +1936,11 @@ query_tree_walker(Query *query,
return true;
if (walker((Node *) query->withCheckOptions, context))
return true;
- if (walker((Node *) query->returningList, context))
- return true;
+ if (!(flags & QTW_IGNORE_RETURNING))
+ {
+ if (walker((Node *) query->returningList, context))
+ return true;
+ }
if (walker((Node *) query->jointree, context))
return true;
if (walker(query->setOperations, context))
@@ -2662,7 +2665,10 @@ query_tree_mutator(Query *query,
MUTATE(query->targetList, query->targetList, List *);
MUTATE(query->withCheckOptions, query->withCheckOptions, List *);
- MUTATE(query->returningList, query->returningList, List *);
+ if (!(flags & QTW_IGNORE_RETURNING))
+ MUTATE(query->returningList, query->returningList, List *);
+ else
+ query->returningList = copyObject(query->returningList);
MUTATE(query->jointree, query->jointree, FromExpr *);
MUTATE(query->setOperations, query->setOperations, Node *);
MUTATE(query->havingQual, query->havingQual, Node *);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index cff4734..53632c1 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -1925,6 +1925,7 @@ _outAppendRelInfo(StringInfo str, const AppendRelInfo *node)
WRITE_UINT_FIELD(parent_relid);
WRITE_UINT_FIELD(child_relid);
+ WRITE_UINT_FIELD(child_result);
WRITE_OID_FIELD(parent_reltype);
WRITE_OID_FIELD(child_reltype);
WRITE_NODE_FIELD(translated_vars);
@@ -2239,6 +2240,7 @@ _outQuery(StringInfo str, const Query *node)
appendStringInfo(str, " :utilityStmt <>");
WRITE_INT_FIELD(resultRelation);
+ WRITE_INT_FIELD(sourceRelation);
WRITE_BOOL_FIELD(hasAggs);
WRITE_BOOL_FIELD(hasWindowFuncs);
WRITE_BOOL_FIELD(hasSubLinks);
@@ -2374,6 +2376,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node)
case RTE_SUBQUERY:
WRITE_NODE_FIELD(subquery);
WRITE_BOOL_FIELD(security_barrier);
+ WRITE_OID_FIELD(rowsec_relid);
break;
case RTE_JOIN:
WRITE_ENUM_FIELD(jointype, JoinType);
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index aad63e5..5d9a9f9 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -199,6 +199,7 @@ _readQuery(void)
READ_BOOL_FIELD(canSetTag);
READ_NODE_FIELD(utilityStmt);
READ_INT_FIELD(resultRelation);
+ READ_INT_FIELD(sourceRelation);
READ_BOOL_FIELD(hasAggs);
READ_BOOL_FIELD(hasWindowFuncs);
READ_BOOL_FIELD(hasSubLinks);
@@ -1213,6 +1214,7 @@ _readRangeTblEntry(void)
case RTE_SUBQUERY:
READ_NODE_FIELD(subquery);
READ_BOOL_FIELD(security_barrier);
+ READ_OID_FIELD(rowsec_relid);
break;
case RTE_JOIN:
READ_ENUM_FIELD(jointype, JoinType);
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index d8aa35d..713f980 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -33,6 +33,7 @@
#include "optimizer/planmain.h"
#include "optimizer/planner.h"
#include "optimizer/prep.h"
+#include "optimizer/rowsecurity.h"
#include "optimizer/subselect.h"
#include "optimizer/tlist.h"
#include "parser/analyze.h"
@@ -177,6 +178,7 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams)
glob->lastPHId = 0;
glob->lastRowMarkId = 0;
glob->transientPlan = false;
+ glob->planUserId = InvalidOid;
/* Determine what fraction of the plan is likely to be scanned */
if (cursorOptions & CURSOR_OPT_FAST_PLAN)
@@ -254,6 +256,7 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams)
result->relationOids = glob->relationOids;
result->invalItems = glob->invalItems;
result->nParamExec = glob->nParamExec;
+ result->planUserId = glob->planUserId;
return result;
}
@@ -404,6 +407,19 @@ subquery_planner(PlannerGlobal *glob, Query *parse,
expand_inherited_tables(root);
/*
+ * Apply row-security policy of the relation being referenced,
+ * if configured with either of built-in or extension's features.
+ * RangeTblEntry of the relation with row-security policy shall
+ * be replaced with a row-security subquery that has simple scan
+ * on the target relation with row-security policy qualifiers.
+ *
+ * This routine assumes PlannerInfo is already handled with
+ * expand_inherited_tables, thus, AppendRelInfo or PlanRowMark
+ * have valid information.
+ */
+ apply_row_security_policy(root);
+
+ /*
* Set hasHavingQual to remember if HAVING clause is present. Needed
* because preprocess_expression will reduce a constant-true condition to
* an empty qual list ... but "HAVING TRUE" is not a semantic no-op.
@@ -888,6 +904,8 @@ inheritance_planner(PlannerInfo *root)
newrti = list_length(subroot.parse->rtable) + 1;
ChangeVarNodes((Node *) subroot.parse, rti, newrti, 0);
ChangeVarNodes((Node *) subroot.rowMarks, rti, newrti, 0);
+ if (subroot.parse->sourceRelation == rti)
+ subroot.parse->sourceRelation = newrti;
rte = copyObject(rte);
subroot.parse->rtable = lappend(subroot.parse->rtable,
rte);
@@ -951,7 +969,10 @@ inheritance_planner(PlannerInfo *root)
root->init_plans = subroot.init_plans;
/* Build list of target-relation RT indexes */
- resultRelations = lappend_int(resultRelations, appinfo->child_relid);
+ resultRelations = lappend_int(resultRelations,
+ (appinfo->child_result > 0 ?
+ appinfo->child_result :
+ appinfo->child_relid));
/* Build lists of per-relation WCO and RETURNING targetlists */
if (parse->withCheckOptions)
diff --git a/src/backend/optimizer/prep/preptlist.c b/src/backend/optimizer/prep/preptlist.c
index fb67f9e..0cccdf5 100644
--- a/src/backend/optimizer/prep/preptlist.c
+++ b/src/backend/optimizer/prep/preptlist.c
@@ -37,8 +37,49 @@
static List *expand_targetlist(List *tlist, int command_type,
- Index result_relation, List *range_table);
+ Index result_relation, Index source_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 Var that references sub-queries being originated from regular
+ * relations with row-level security policy due to nature of sub-query
+ * that has no system-column.
+ */
+static AttrNumber
+lookup_varattno(AttrNumber attno, Index rt_index, List *rtables)
+{
+ RangeTblEntry *rte = rt_fetch(rt_index, rtables);
+
+ if (rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_SECURITY)
+ {
+ ListCell *cell;
+
+ foreach (cell, rte->subquery->targetList)
+ {
+ TargetEntry *tle = lfirst(cell);
+ Var *var;
+
+ if (IsA(tle->expr, Const))
+ continue;
+
+ var = (Var *) tle->expr;
+ Assert(IsA(var, Var));
+
+ if (var->varattno == attno)
+ return tle->resno;
+ }
+ elog(ERROR, "invalid attno %d on row-security subquery target-list",
+ attno);
+ }
+ return attno;
+}
/*
* preprocess_targetlist
@@ -51,6 +92,7 @@ preprocess_targetlist(PlannerInfo *root, List *tlist)
{
Query *parse = root->parse;
int result_relation = parse->resultRelation;
+ int source_relation = parse->sourceRelation;
List *range_table = parse->rtable;
CmdType command_type = parse->commandType;
ListCell *lc;
@@ -73,8 +115,12 @@ preprocess_targetlist(PlannerInfo *root, List *tlist)
* 10/94
*/
if (command_type == CMD_INSERT || command_type == CMD_UPDATE)
+ {
tlist = expand_targetlist(tlist, command_type,
- result_relation, range_table);
+ result_relation,
+ source_relation,
+ range_table);
+ }
/*
* Add necessary junk columns for rowmarked rels. These values are needed
@@ -96,7 +142,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,
@@ -112,7 +159,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,
@@ -195,7 +243,8 @@ preprocess_targetlist(PlannerInfo *root, List *tlist)
*/
static List *
expand_targetlist(List *tlist, int command_type,
- Index result_relation, List *range_table)
+ Index result_relation, Index source_relation,
+ List *range_table)
{
List *new_tlist = NIL;
ListCell *tlist_item;
@@ -218,6 +267,9 @@ expand_targetlist(List *tlist, int command_type,
numattrs = RelationGetNumberOfAttributes(rel);
+ if (source_relation == 0)
+ source_relation = result_relation;
+
for (attrno = 1; attrno <= numattrs; attrno++)
{
Form_pg_attribute att_tup = rel->rd_att->attrs[attrno - 1];
@@ -298,8 +350,10 @@ expand_targetlist(List *tlist, int command_type,
case CMD_UPDATE:
if (!att_tup->attisdropped)
{
- new_expr = (Node *) makeVar(result_relation,
- attrno,
+ new_expr = (Node *) makeVar(source_relation,
+ lookup_varattno(attrno,
+ source_relation,
+ range_table),
atttype,
atttypmod,
attcollation,
diff --git a/src/backend/optimizer/prep/prepunion.c b/src/backend/optimizer/prep/prepunion.c
index e249628..017bc2c 100644
--- a/src/backend/optimizer/prep/prepunion.c
+++ b/src/backend/optimizer/prep/prepunion.c
@@ -55,6 +55,7 @@ typedef struct
{
PlannerInfo *root;
AppendRelInfo *appinfo;
+ bool in_returning;
} adjust_appendrel_attrs_context;
static Plan *recurse_set_operations(Node *setOp, PlannerInfo *root,
@@ -1594,6 +1595,7 @@ adjust_appendrel_attrs(PlannerInfo *root, Node *node, AppendRelInfo *appinfo)
context.root = root;
context.appinfo = appinfo;
+ context.in_returning = false;
/*
* Must be prepared to start with a Query or a bare expression tree.
@@ -1605,10 +1607,29 @@ adjust_appendrel_attrs(PlannerInfo *root, Node *node, AppendRelInfo *appinfo)
newnode = query_tree_mutator((Query *) node,
adjust_appendrel_attrs_mutator,
(void *) &context,
- QTW_IGNORE_RC_SUBQUERIES);
+ QTW_IGNORE_RC_SUBQUERIES |
+ QTW_IGNORE_RETURNING);
+ /*
+ * Returning clause on the relation being replaced with row-
+ * security subquery shall be handled in a special way, because
+ * of no system columns on subquery.
+ * Var references to system column or whole-row reference need
+ * to be adjusted to reference pseudo columns on behalf of
+ * the underlying these columns, however, RETURNGIN clause is
+ * an exception because its Var nodes are evaluated towards
+ * the "raw" target relation, not a fetched tuple.
+ */
+ context.in_returning = true;
+ newnode->returningList = (List *)
+ expression_tree_mutator((Node *) newnode->returningList,
+ adjust_appendrel_attrs_mutator,
+ (void *) &context);
if (newnode->resultRelation == appinfo->parent_relid)
{
- newnode->resultRelation = appinfo->child_relid;
+ newnode->resultRelation = (appinfo->child_result > 0 ?
+ appinfo->child_result :
+ appinfo->child_relid);
+ newnode->sourceRelation = appinfo->child_relid;
/* Fix tlist resnos too, if it's inherited UPDATE */
if (newnode->commandType == CMD_UPDATE)
newnode->targetList =
@@ -1624,6 +1645,49 @@ adjust_appendrel_attrs(PlannerInfo *root, Node *node, AppendRelInfo *appinfo)
}
static Node *
+fixup_var_on_rowsec_subquery(RangeTblEntry *rte, Var *var)
+{
+ ListCell *cell;
+
+ Assert(rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_SECURITY);
+ /*
+ * In case when row-level security policy is applied on the referenced
+ * table, its RangeTblEntry (RTE_RELATION) is replaced with sub-query
+ * to filter out unprivileged rows of underlying relation.
+ * Even though reference to this sub-query should perform as if ones
+ * to real relations, system column has to be cared in special way
+ * due to the nature of sub-query.
+ * Target-entries that reference system columns should be added on
+ * rowlevelsec.c, so all we need to do here is looking up underlying
+ * target-list that can reference underlying system column, and fix-
+ * up varattno of the referencing Var node with resno of TargetEntry.
+ */
+ foreach (cell, rte->subquery->targetList)
+ {
+ TargetEntry *subtle = lfirst(cell);
+
+ if (IsA(subtle->expr, Var))
+ {
+ Var *subvar = (Var *) subtle->expr;
+ Var *newnode;
+
+ if (subvar->varattno == var->varattno)
+ {
+ newnode = copyObject(var);
+ newnode->varattno = subtle->resno;
+ return (Node *)newnode;
+ }
+ }
+ else
+ Assert(IsA(subtle->expr, Const));
+ }
+ elog(ERROR, "could not find pseudo column of %d, relid %u",
+ var->varattno, var->varno);
+ return NULL;
+}
+
+static Node *
adjust_appendrel_attrs_mutator(Node *node,
adjust_appendrel_attrs_context *context)
{
@@ -1638,8 +1702,12 @@ adjust_appendrel_attrs_mutator(Node *node,
if (var->varlevelsup == 0 &&
var->varno == appinfo->parent_relid)
{
- var->varno = appinfo->child_relid;
+ var->varno = (context->in_returning &&
+ appinfo->child_result > 0 ?
+ appinfo->child_result :
+ appinfo->child_relid);
var->varnoold = appinfo->child_relid;
+
if (var->varattno > 0)
{
Node *newnode;
@@ -1664,6 +1732,14 @@ adjust_appendrel_attrs_mutator(Node *node,
*/
if (OidIsValid(appinfo->child_reltype))
{
+ Query *parse = context->root->parse;
+ RangeTblEntry *rte = rt_fetch(appinfo->child_relid,
+ parse->rtable);
+ if (!context->in_returning &&
+ rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_SECURITY)
+ var = (Var *)fixup_var_on_rowsec_subquery(rte, var);
+
Assert(var->vartype == appinfo->parent_reltype);
if (appinfo->parent_reltype != appinfo->child_reltype)
{
@@ -1708,7 +1784,18 @@ adjust_appendrel_attrs_mutator(Node *node,
return (Node *) rowexpr;
}
}
- /* system attributes don't need any other translation */
+ else
+ {
+ Query *parse = context->root->parse;
+ RangeTblEntry *rte;
+
+ rte = rt_fetch(appinfo->child_relid, parse->rtable);
+
+ if (!context->in_returning &&
+ rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_SECURITY)
+ return fixup_var_on_rowsec_subquery(rte, var);
+ }
}
return (Node *) var;
}
diff --git a/src/backend/optimizer/util/Makefile b/src/backend/optimizer/util/Makefile
index 3b2d16b..3f5cb19 100644
--- a/src/backend/optimizer/util/Makefile
+++ b/src/backend/optimizer/util/Makefile
@@ -13,6 +13,6 @@ top_builddir = ../../../..
include $(top_builddir)/src/Makefile.global
OBJS = clauses.o joininfo.o pathnode.o placeholder.o plancat.o predtest.o \
- relnode.o restrictinfo.o tlist.o var.o
+ relnode.o restrictinfo.o tlist.o var.o rowsecurity.o
include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/optimizer/util/rowsecurity.c b/src/backend/optimizer/util/rowsecurity.c
new file mode 100644
index 0000000..c2e5a49
--- /dev/null
+++ b/src/backend/optimizer/util/rowsecurity.c
@@ -0,0 +1,744 @@
+/*
+ * optimizer/util/rowsecurity.c
+ * Routines to support row-security feature
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ */
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "access/htup_details.h"
+#include "access/sysattr.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_inherits_fn.h"
+#include "catalog/pg_rowsecurity.h"
+#include "catalog/pg_type.h"
+#include "miscadmin.h"
+#include "nodes/makefuncs.h"
+#include "nodes/nodeFuncs.h"
+#include "nodes/plannodes.h"
+#include "optimizer/clauses.h"
+#include "optimizer/prep.h"
+#include "optimizer/rowsecurity.h"
+#include "parser/parsetree.h"
+#include "rewrite/rewriteHandler.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "tcop/utility.h"
+
+/* flags to pull row-security policy */
+#define RSEC_FLAG_HAS_SUBLINKS 0x0001
+
+/* hook to allow extensions to apply their own security policy */
+row_security_policy_hook_type row_security_policy_hook = NULL;
+
+/*
+ * make_pseudo_column
+ *
+ * It makes a target-entry node that references underlying column.
+ * Its tle->expr is usualy Var node, but may be Const for dummy NULL
+ * if the supplied attribute was already dropped.
+ */
+static TargetEntry *
+make_pseudo_column(RangeTblEntry *subrte, AttrNumber attnum)
+{
+ Expr *expr;
+ char *resname;
+
+ Assert(subrte->rtekind == RTE_RELATION && OidIsValid(subrte->relid));
+ if (attnum == InvalidAttrNumber)
+ {
+ expr = (Expr *) makeWholeRowVar(subrte, (Index) 1, 0, false);
+ resname = get_rel_name(subrte->relid);
+ }
+ else
+ {
+ HeapTuple tuple;
+ Form_pg_attribute attform;
+
+ tuple = SearchSysCache2(ATTNUM,
+ ObjectIdGetDatum(subrte->relid),
+ Int16GetDatum(attnum));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for attribute %d of relation %u",
+ attnum, subrte->relid);
+ attform = (Form_pg_attribute) GETSTRUCT(tuple);
+
+ if (attform->attisdropped)
+ {
+ char namebuf[NAMEDATALEN];
+
+ /* Insert NULL just for a placeholder of dropped column */
+ expr = (Expr *) makeConst(INT4OID,
+ -1,
+ InvalidOid,
+ sizeof(int32),
+ (Datum) 0,
+ true, /* isnull */
+ true); /* byval */
+ sprintf(namebuf, "dummy-%d", (int)attform->attnum);
+ resname = pstrdup(namebuf);
+ }
+ else
+ {
+ expr = (Expr *) makeVar((Index) 1,
+ attform->attnum,
+ attform->atttypid,
+ attform->atttypmod,
+ attform->attcollation,
+ 0);
+ resname = pstrdup(NameStr(attform->attname));
+ }
+ ReleaseSysCache(tuple);
+ }
+ return makeTargetEntry(expr, -1, resname, false);
+}
+
+/*
+ * lookup_pseudo_column
+ *
+ * It looks-up resource number of the target-entry relevant to the given
+ * Var-node that references the row-security subquery. If required column
+ * is not in the subquery's target-list, this function also adds new one
+ * and returns its resource number.
+ */
+static AttrNumber
+lookup_pseudo_column(PlannerInfo *root,
+ RangeTblEntry *rte, AttrNumber varattno)
+{
+ Query *subqry;
+ RangeTblEntry *subrte;
+ TargetEntry *subtle;
+ ListCell *cell;
+
+ Assert(rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_SECURITY);
+
+ subqry = rte->subquery;
+ foreach (cell, subqry->targetList)
+ {
+ subtle = lfirst(cell);
+
+ /*
+ * If referenced artifical column is already constructed on the
+ * target-list of row-security subquery, nothing to do any more.
+ */
+ if (IsA(subtle->expr, Var))
+ {
+ Var *subvar = (Var *)subtle->expr;
+
+ Assert(subvar->varno == 1);
+ if (subvar->varattno == varattno)
+ return subtle->resno;
+ }
+ }
+
+ /*
+ * OK, we don't have an artifical column relevant to the required ones,
+ * so let's create a new artifical column on demand.
+ */
+ subrte = rt_fetch((Index) 1, subqry->rtable);
+ subtle = make_pseudo_column(subrte, varattno);
+ subtle->resno = list_length(subqry->targetList) + 1;
+
+ subqry->targetList = lappend(subqry->targetList, subtle);
+ rte->eref->colnames = lappend(rte->eref->colnames,
+ makeString(pstrdup(subtle->resname)));
+ return subtle->resno;
+}
+
+/*
+ * fixup_varnode_walker
+ *
+ * It recursively fixes up references to the relation to be replaced by
+ * row-security sub-query, and adds pseudo columns relevant to the
+ * underlying system columns or whole row-reference on demand.
+ */
+typedef struct {
+ PlannerInfo *root;
+ int varlevelsup;
+ Index *vartrans;
+} fixup_varnode_context;
+
+static bool
+fixup_varnode_walker(Node *node, fixup_varnode_context *context)
+{
+ if (node == NULL)
+ return false;
+
+ if (IsA(node, Var))
+ {
+ Var *var = (Var *) node;
+ List *rtable = context->root->parse->rtable;
+ RangeTblEntry *rte;
+ ListCell *cell;
+
+ /*
+ * Ignore it, if Var node does not reference the Query currently
+ * we focus on.
+ */
+ if (var->varlevelsup != context->varlevelsup)
+ return false;
+
+ if (context->vartrans[var->varno] > 0)
+ {
+ Index rtindex_trans = context->vartrans[var->varno];
+
+ rte = rt_fetch(rtindex_trans, rtable);
+ Assert(rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_SECURITY);
+
+ var->varno = var->varnoold = rtindex_trans;
+ var->varattno = lookup_pseudo_column(context->root, rte,
+ var->varattno);
+ }
+ else
+ {
+ rte = rt_fetch(var->varno, rtable);
+ if (rte->rtekind == RTE_RELATION && rte->inh)
+ {
+ foreach (cell, context->root->append_rel_list)
+ {
+ AppendRelInfo *appinfo = lfirst(cell);
+ RangeTblEntry *child_rte;
+
+ if (appinfo->parent_relid != var->varno)
+ continue;
+
+ child_rte = rt_fetch(appinfo->child_relid, rtable);
+ if (child_rte->rtekind == RTE_SUBQUERY &&
+ child_rte->subquery->querySource == QSRC_ROW_SECURITY)
+ (void) lookup_pseudo_column(context->root,
+ child_rte,
+ var->varattno);
+ }
+ }
+ }
+ }
+ else if (IsA(node, RangeTblRef))
+ {
+ RangeTblRef *rtr = (RangeTblRef *) node;
+
+ if (context->varlevelsup == 0 &&
+ context->vartrans[rtr->rtindex] != 0)
+ rtr->rtindex = context->vartrans[rtr->rtindex];
+ }
+ else if (IsA(node, Query))
+ {
+ bool result;
+
+ context->varlevelsup++;
+ result = query_tree_walker((Query *) node,
+ fixup_varnode_walker,
+ (void *) context, 0);
+ context->varlevelsup--;
+
+ return result;
+ }
+ return expression_tree_walker(node,
+ fixup_varnode_walker,
+ (void *) context);
+}
+
+/*
+ * check_infinite_recursion
+ *
+ * It is a wrong row-security configuration, if we try to expand
+ * the relation inside of row-security subquery originated from
+ * same relation!
+ */
+static void
+check_infinite_recursion(PlannerInfo *root, Oid relid)
+{
+ PlannerInfo *parent = root->parent_root;
+
+ if (parent && parent->parse->querySource == QSRC_ROW_SECURITY)
+ {
+ RangeTblEntry *rte = rt_fetch(1, parent->parse->rtable);
+
+ Assert(rte->rtekind == RTE_RELATION && OidIsValid(rte->relid));
+
+ if (relid == rte->relid)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("infinite recursion detected for relation \"%s\"",
+ get_rel_name(relid))));
+ check_infinite_recursion(parent, relid);
+ }
+}
+
+/*
+ * expand_rtentry_with_policy
+ *
+ * It extends a range-table entry of row-security sub-query with supplied
+ * security policy, and append it on the parse->rtable.
+ * This sub-query contains pseudo columns that reference underlying
+ * regular columns (at least, references to system column or whole of
+ * table reference shall be added on demand), and simple scan on the
+ * target relation.
+ * Any Var nodes that referenced the relation pointed by rtindex shall
+ * be adjusted to reference this sub-query instead. walker
+ */
+static Index
+expand_rtentry_with_policy(PlannerInfo *root, Index rtindex,
+ Expr *qual, int flags)
+{
+ Query *parse = root->parse;
+ RangeTblEntry *rte = rt_fetch(rtindex, parse->rtable);
+ Query *subqry;
+ RangeTblEntry *subrte;
+ RangeTblRef *subrtr;
+ TargetEntry *subtle;
+ RangeTblEntry *newrte;
+ HeapTuple tuple;
+ AttrNumber nattrs;
+ AttrNumber attnum;
+ List *targetList = NIL;
+ List *colNameList = NIL;
+ PlanRowMark *rowmark;
+
+ Assert(rte->rtekind == RTE_RELATION && !rte->inh);
+
+ /* check recursion to prevent infinite loop */
+ check_infinite_recursion(root, rte->relid);
+
+ /* Expand views inside SubLink node */
+ if (flags & RSEC_FLAG_HAS_SUBLINKS)
+ QueryRewriteExpr((Node *)qual, list_make1_oid(rte->relid));
+
+ /*
+ * Construction of sub-query
+ */
+ subqry = (Query *) makeNode(Query);
+ subqry->commandType = CMD_SELECT;
+ subqry->querySource = QSRC_ROW_SECURITY;
+
+ subrte = copyObject(rte);
+ subqry->rtable = list_make1(subrte);
+
+ subrtr = makeNode(RangeTblRef);
+ subrtr->rtindex = 1;
+ subqry->jointree = makeFromExpr(list_make1(subrtr), (Node *) qual);
+ if (flags & RSEC_FLAG_HAS_SUBLINKS)
+ subqry->hasSubLinks = true;
+
+ /*
+ * Construction of TargetEntries that reference underlying columns.
+ */
+ tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(rte->relid));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for relation %u", rte->relid);
+ nattrs = ((Form_pg_class) GETSTRUCT(tuple))->relnatts;
+ ReleaseSysCache(tuple);
+
+ for (attnum = 1; attnum <= nattrs; attnum++)
+ {
+ subtle = make_pseudo_column(subrte, attnum);
+ subtle->resno = list_length(targetList) + 1;
+ Assert(subtle->resno == attnum);
+
+ targetList = lappend(targetList, subtle);
+ colNameList = lappend(colNameList,
+ makeString(pstrdup(subtle->resname)));
+ }
+ subqry->targetList = targetList;
+
+ /* Expand RengeTblEntry with this sub-query */
+ newrte = makeNode(RangeTblEntry);
+ newrte->rtekind = RTE_SUBQUERY;
+ newrte->subquery = subqry;
+ newrte->security_barrier = true;
+ newrte->rowsec_relid = rte->relid;
+ newrte->eref = makeAlias(get_rel_name(rte->relid), colNameList);
+
+ parse->rtable = lappend(parse->rtable, newrte);
+
+ /*
+ * Fix up PlanRowMark if needed, then add references to 'tableoid' and
+ * 'ctid' that shall be added to handle row-level locking.
+ * Also see preprocess_targetlist() that adds some junk attributes.
+ */
+ rowmark = get_plan_rowmark(root->rowMarks, rtindex);
+ if (rowmark)
+ {
+ if (rowmark->rti == rowmark->prti)
+ rowmark->rti = rowmark->prti = list_length(parse->rtable);
+ else
+ rowmark->rti = list_length(parse->rtable);
+
+ lookup_pseudo_column(root, newrte, SelfItemPointerAttributeNumber);
+ lookup_pseudo_column(root, newrte, TableOidAttributeNumber);
+ }
+ return list_length(parse->rtable);
+}
+
+/*
+ * pull_row_security_policy
+ *
+ * It pulls the configured row-security policy of both built-in and
+ * extensions. If any, it returns expression tree.
+ */
+static Expr *
+pull_row_security_policy(CmdType cmd, Relation relation, int *p_flags)
+{
+ Expr *quals = NULL;
+ int flags = 0;
+
+ /*
+ * Pull the row-security policy configured with built-in features,
+ * if unprivileged users. Please note that superuser can bypass it.
+ */
+ if (relation->rsdesc && !superuser())
+ {
+ RowSecurityDesc *rsdesc = relation->rsdesc;
+
+ quals = copyObject(rsdesc->rsall.qual);
+ if (rsdesc->rsall.hassublinks)
+ flags |= RSEC_FLAG_HAS_SUBLINKS;
+ }
+
+ /*
+ * Also, ask extensions whether they want to apply their own
+ * row-security policy. If both built-in and extension has
+ * their own policy, it shall be merged.
+ */
+ if (row_security_policy_hook)
+ {
+ List *temp;
+
+ temp = (*row_security_policy_hook)(cmd, relation);
+ if (temp != NIL)
+ {
+ if ((flags & RSEC_FLAG_HAS_SUBLINKS) == 0 &&
+ contain_subplans((Node *) temp))
+ flags |= RSEC_FLAG_HAS_SUBLINKS;
+
+ if (quals != NULL)
+ temp = lappend(temp, quals);
+
+ if (list_length(temp) == 1)
+ quals = (Expr *)list_head(temp);
+ else if (list_length(temp) > 1)
+ quals = makeBoolExpr(AND_EXPR, temp, -1);
+ }
+ }
+ *p_flags = flags;
+ return quals;
+}
+
+/*
+ * copy_row_security_policy
+ *
+ * It construct a row-security subquery instead of raw COPY TO statement,
+ * if target relation has a row-level security policy
+ */
+bool
+copy_row_security_policy(CopyStmt *stmt, Relation rel, List *attnums)
+{
+ Expr *quals;
+ int flags;
+ Query *parse;
+ RangeTblEntry *rte;
+ RangeTblRef *rtr;
+ TargetEntry *tle;
+ Var *var;
+ ListCell *cell;
+
+ if (stmt->is_from)
+ return false;
+
+ quals = pull_row_security_policy(CMD_SELECT, rel, &flags);
+ if (!quals)
+ return false;
+
+ parse = (Query *) makeNode(Query);
+ parse->commandType = CMD_SELECT;
+ parse->querySource = QSRC_ROW_SECURITY;
+
+ rte = makeNode(RangeTblEntry);
+ rte->rtekind = RTE_RELATION;
+ rte->relid = RelationGetRelid(rel);
+ rte->relkind = RelationGetForm(rel)->relkind;
+
+ foreach (cell, attnums)
+ {
+ HeapTuple tuple;
+ Form_pg_attribute attform;
+ AttrNumber attno = lfirst_int(cell);
+
+ tuple = SearchSysCache2(ATTNUM,
+ ObjectIdGetDatum(RelationGetRelid(rel)),
+ Int16GetDatum(attno));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for attribute %d of relation %s",
+ attno, RelationGetRelationName(rel));
+ attform = (Form_pg_attribute) GETSTRUCT(tuple);
+
+ var = makeVar((Index) 1,
+ attform->attnum,
+ attform->atttypid,
+ attform->atttypmod,
+ attform->attcollation,
+ 0);
+ tle = makeTargetEntry((Expr *) var,
+ list_length(parse->targetList) + 1,
+ pstrdup(NameStr(attform->attname)),
+ false);
+ parse->targetList = lappend(parse->targetList, tle);
+
+ ReleaseSysCache(tuple);
+
+ rte->selectedCols = bms_add_member(rte->selectedCols,
+ attno - FirstLowInvalidHeapAttributeNumber);
+ }
+ rte->inFromCl = true;
+ rte->requiredPerms = ACL_SELECT;
+
+ rtr = makeNode(RangeTblRef);
+ rtr->rtindex = 1;
+
+ parse->jointree = makeFromExpr(list_make1(rtr), (Node *) quals);
+ parse->rtable = list_make1(rte);
+ if (flags & RSEC_FLAG_HAS_SUBLINKS)
+ parse->hasSubLinks = true;
+
+ stmt->query = (Node *) parse;
+
+ return true;
+}
+
+/*
+ * apply_row_security_relation
+ *
+ * It applies row-security policy on a particular relation being specified.
+ * If this relation is top of the inheritance tree, it also checks inherited
+ * children.
+ */
+static bool
+apply_row_security_relation(PlannerInfo *root, Index *vartrans,
+ CmdType cmd, Index rtindex)
+{
+ Query *parse = root->parse;
+ RangeTblEntry *rte = rt_fetch(rtindex, parse->rtable);
+ Relation rel;
+ Expr *qual;
+ int flags;
+ bool result = false;
+
+ if (!rte->inh)
+ {
+ rel = heap_open(rte->relid, NoLock);
+ qual = pull_row_security_policy(cmd, rel, &flags);
+ if (qual)
+ {
+ vartrans[rtindex]
+ = expand_rtentry_with_policy(root, rtindex, qual, flags);
+ if (parse->resultRelation == rtindex)
+ parse->sourceRelation = vartrans[rtindex];
+ result = true;
+ }
+ heap_close(rel, NoLock);
+ }
+ else
+ {
+ /*
+ * In case when relation has inherited children, we try to apply
+ * row-level security policy of them if configured.
+ * In addition to regular replacement with a sub-query, we need
+ * to adjust rtindex of AppendRelInfo and varno of translated_vars.
+ * It makes sub-queries perform like regular relations being
+ * inherited from a particular parent relation. So, a table scan
+ * may have underlying a relation scan and two sub-query scans for
+ * instance. If it is result relation of UPDATE or DELETE command,
+ * rtindex to the original relation (regular relation) has to be
+ * kept because sub-query cannot perform as an updatable relation.
+ * So, we save it on child_result of AppendRelInfo; that shall be
+ * used to track relations to be modified at inheritance_planner().
+ */
+ ListCell *lc1, *lc2;
+
+ foreach (lc1, root->append_rel_list)
+ {
+ AppendRelInfo *apinfo = lfirst(lc1);
+
+ if (apinfo->parent_relid != rtindex)
+ continue;
+
+ if (apply_row_security_relation(root, vartrans, cmd,
+ apinfo->child_relid))
+ {
+ /*
+ * Save the rtindex of actual relation to be modified,
+ * if parent relation is result relation of this query.
+ */
+ if (parse->resultRelation == rtindex)
+ apinfo->child_result = apinfo->child_relid;
+
+ apinfo->child_relid = vartrans[apinfo->child_relid];
+ /* Adjust varno to reference pseudo columns */
+ foreach (lc2, apinfo->translated_vars)
+ {
+ Var *var = lfirst(lc2);
+
+ if (var)
+ var->varno = apinfo->child_relid;
+ }
+ result = true;
+ }
+ }
+ }
+ return result;
+}
+
+/*
+ * apply_row_security_recursive
+ *
+ * It walks on the given join-tree to replace relations with row-level
+ * security policy by a simple sub-query.
+ */
+static bool
+apply_row_security_recursive(PlannerInfo *root, Index *vartrans, Node *jtnode)
+{
+ bool result = false;
+
+ if (jtnode == NULL)
+ return false;
+ if (IsA(jtnode, RangeTblRef))
+ {
+ Index rtindex = ((RangeTblRef *) jtnode)->rtindex;
+ Query *parse = root->parse;
+ RangeTblEntry *rte = rt_fetch(rtindex, parse->rtable);
+ CmdType cmd;
+
+ /* Only relation can have row-security policy */
+ if (rte->rtekind != RTE_RELATION)
+ return false;
+
+ /*
+ * Prevents infinite recursion. Please note that rtindex == 1
+ * of the row-security subquery is a relation being already
+ * processed on the upper level.
+ */
+ if (parse->querySource == QSRC_ROW_SECURITY && rtindex == 1)
+ return false;
+
+ /* Is it a result relation of UPDATE or DELETE command? */
+ if (parse->resultRelation == rtindex)
+ cmd = parse->commandType;
+ else
+ cmd = CMD_SELECT;
+
+ /* Try to apply row-security policy, if configured */
+ result = apply_row_security_relation(root, vartrans, cmd, rtindex);
+ }
+ else if (IsA(jtnode, FromExpr))
+ {
+ FromExpr *f = (FromExpr *) jtnode;
+ ListCell *l;
+
+ foreach (l, f->fromlist)
+ {
+ if (apply_row_security_recursive(root, vartrans, lfirst(l)))
+ result = true;
+ }
+ }
+ else if (IsA(jtnode, JoinExpr))
+ {
+ JoinExpr *j = (JoinExpr *) jtnode;
+
+ if (apply_row_security_recursive(root, vartrans, j->larg))
+ result = true;
+ if (apply_row_security_recursive(root, vartrans, j->rarg))
+ result = true;
+ }
+ else
+ elog(ERROR, "unexpected node type: %d", (int) nodeTag(jtnode));
+
+ return result;
+}
+
+/*
+ * apply_row_security_policy
+ *
+ * Entrypoint to apply configured row-security policy of the relation.
+ *
+ * In case when the supplied query references relations with row-security
+ * policy, its RangeTblEntry shall be replaced by a row-security subquery
+ * that has simple scan on the referenced table with policy qualifiers.
+ * Of course, security-barrier shall be set on the subquery to prevent
+ * unexpected push-down of functions without leakproof flag.
+ *
+ * For example, when table t1 has a security policy "(x % 2 = 0)", the
+ * following query:
+ * SELECT * FROM t1 WHERE f_leak(y)
+ * performs as if
+ * SELECT * FROM (
+ * SELECT x, y FROM t1 WHERE (x % 2 = 0)
+ * ) AS t1 WHERE f_leak(y)
+ * would be given. Because the sub-query has security barrier flag,
+ * configured security policy qualifier is always executed prior to
+ * user given functions.
+ */
+void
+apply_row_security_policy(PlannerInfo *root)
+{
+ Query *parse = root->parse;
+ Oid curr_userid;
+ int curr_seccxt;
+ Index *vartrans;
+
+ /*
+ * Mode checks. In case when SECURITY_ROW_LEVEL_DISABLED is set,
+ * no row-level security policy should be applied regardless
+ * whether it is built-in or extension.
+ */
+ GetUserIdAndSecContext(&curr_userid, &curr_seccxt);
+ if (curr_seccxt & SECURITY_ROW_LEVEL_DISABLED)
+ return;
+
+ vartrans = palloc0(sizeof(Index) * (list_length(parse->rtable) + 1));
+ if (apply_row_security_recursive(root, vartrans, (Node *)parse->jointree))
+ {
+ PlannerGlobal *glob = root->glob;
+ PlanInvalItem *pi_item;
+ fixup_varnode_context context;
+
+ /*
+ * Constructed Plan with row-level security policy depends on
+ * properties of current user (database superuser can bypass
+ * configured row-security policy!), thus, it has to be
+ * invalidated when its assumption was changed.
+ */
+ if (!OidIsValid(glob->planUserId))
+ {
+ /* Plan invalidation on session user-id */
+ glob->planUserId = GetUserId();
+
+ /* Plan invalidation on catalog updates of pg_authid */
+ pi_item = makeNode(PlanInvalItem);
+ pi_item->cacheId = AUTHOID;
+ pi_item->hashValue =
+ GetSysCacheHashValue1(AUTHOID,
+ ObjectIdGetDatum(glob->planUserId));
+ glob->invalItems = lappend(glob->invalItems, pi_item);
+ }
+ else
+ Assert(glob->planUserId == GetUserId());
+
+ /*
+ * Var-nodes that referenced RangeTblEntry to be replaced by
+ * row-security sub-query have to be adjusted for appropriate
+ * reference to the underlying pseudo column of the relation.
+ */
+ context.root = root;
+ context.varlevelsup = 0;
+ context.vartrans = vartrans;
+ query_tree_walker(parse,
+ fixup_varnode_walker,
+ (void *) &context,
+ QTW_IGNORE_RETURNING);
+ }
+ pfree(vartrans);
+}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 22e82ba..592671a 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -256,6 +256,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
%type <node> alter_table_cmd alter_type_cmd opt_collate_clause
%type <list> alter_table_cmds alter_type_cmds
+%type <str> row_security_cmd
%type <dbehavior> opt_drop_behavior
@@ -2173,6 +2174,24 @@ alter_table_cmd:
n->def = (Node *)$2;
$$ = (Node *)n;
}
+ /* ALTER TABLE <name> SET ROW SECURITY FOR <cmd> TO (<expr>) */
+ | SET ROW SECURITY FOR row_security_cmd TO '(' a_expr ')'
+ {
+ AlterTableCmd *n = makeNode(AlterTableCmd);
+ n->subtype = AT_SetRowSecurity;
+ n->name = $5;
+ n->def = (Node *) $8;
+ $$ = (Node *)n;
+ }
+ /* ALTER TABLE <name> RESET ROW SECURITY FOR <cmd> */
+ | RESET ROW SECURITY FOR row_security_cmd
+ {
+ AlterTableCmd *n = makeNode(AlterTableCmd);
+ n->subtype = AT_ResetRowSecurity;
+ n->name = $5;
+ n->def = NULL;
+ $$ = (Node *)n;
+ }
| alter_generic_options
{
AlterTableCmd *n = makeNode(AlterTableCmd);
@@ -2244,6 +2263,12 @@ reloption_elem:
}
;
+row_security_cmd: ALL { $$ = "all"; }
+ | SELECT { $$ = "select"; }
+ | INSERT { $$ = "insert"; }
+ | UPDATE { $$ = "update"; }
+ | DELETE_P { $$ = "delete"; }
+ ;
/*****************************************************************************
*
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
index 4e4e1cd..d9049e0 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -272,6 +272,9 @@ transformAggregateCall(ParseState *pstate, Aggref *agg,
case EXPR_KIND_TRIGGER_WHEN:
err = _("aggregate functions are not allowed in trigger WHEN conditions");
break;
+ case EXPR_KIND_ROW_SECURITY:
+ err = _("aggregate functions are not allowed in row-security policy");
+ break;
/*
* There is intentionally no default: case here, so that the
@@ -547,6 +550,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
case EXPR_KIND_TRIGGER_WHEN:
err = _("window functions are not allowed in trigger WHEN conditions");
break;
+ case EXPR_KIND_ROW_SECURITY:
+ err = _("window functions are not allowed in row-security policy");
+ break;
/*
* There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 68b711d..fb09c8c 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -1459,6 +1459,7 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
case EXPR_KIND_OFFSET:
case EXPR_KIND_RETURNING:
case EXPR_KIND_VALUES:
+ case EXPR_KIND_ROW_SECURITY:
/* okay */
break;
case EXPR_KIND_CHECK_CONSTRAINT:
@@ -2640,6 +2641,8 @@ ParseExprKindName(ParseExprKind exprKind)
return "EXECUTE";
case EXPR_KIND_TRIGGER_WHEN:
return "WHEN";
+ case EXPR_KIND_ROW_SECURITY:
+ return "ROW SECURITY";
/*
* There is intentionally no default: case here, so that the
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index c55fb14..218416a 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -3097,3 +3097,19 @@ QueryRewrite(Query *parsetree)
return results;
}
+
+/*
+ * QueryRewriteExpr
+ *
+ * This routine provides an entry point of query rewriter towards
+ * a certain expression tree with SubLink node; being added after
+ * the top level query rewrite.
+ * It primarily intends to expand views appeared in the qualifiers
+ * appended with row-level security which needs to modify query
+ * tree at head of the planner stage.
+ */
+void
+QueryRewriteExpr(Node *node, List *activeRIRs)
+{
+ fireRIRonSubLink(node, activeRIRs);
+}
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index 65edc1f..b36da97 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -3008,6 +3008,7 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
int spi_result;
Oid save_userid;
int save_sec_context;
+ int temp_sec_context;
Datum vals[RI_MAX_NUMKEYS * 2];
char nulls[RI_MAX_NUMKEYS * 2];
@@ -3087,8 +3088,18 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
/* Switch to proper UID to perform check as */
GetUserIdAndSecContext(&save_userid, &save_sec_context);
+
+ /*
+ * Row-level security should be disabled in case when foreign-key
+ * relation is queried to check existence of tuples that references
+ * the primary-key being modified.
+ */
+ temp_sec_context = save_sec_context | SECURITY_LOCAL_USERID_CHANGE;
+ if (source_is_pk)
+ temp_sec_context |= SECURITY_ROW_LEVEL_DISABLED;
+
SetUserIdAndSecContext(RelationGetForm(query_rel)->relowner,
- save_sec_context | SECURITY_LOCAL_USERID_CHANGE);
+ temp_sec_context);
/* Finally we can run the query. */
spi_result = SPI_execute_snapshot(qplan,
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index cf740a9..f1e67a5 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -53,6 +53,7 @@
#include "catalog/namespace.h"
#include "executor/executor.h"
#include "executor/spi.h"
+#include "miscadmin.h"
#include "nodes/nodeFuncs.h"
#include "optimizer/cost.h"
#include "optimizer/planmain.h"
@@ -795,6 +796,16 @@ CheckCachedPlan(CachedPlanSource *plansource)
AcquireExecutorLocks(plan->stmt_list, true);
/*
+ * If plan was constructed with assumption of a particular user-id,
+ * and it is different from the current one, the cached-plan shall
+ * be invalidated to construct suitable query plan.
+ */
+ if (plan->is_valid &&
+ OidIsValid(plan->planUserId) &&
+ plan->planUserId == GetUserId())
+ plan->is_valid = false;
+
+ /*
* If plan was transient, check to see if TransactionXmin has
* advanced, and if so invalidate it.
*/
@@ -847,6 +858,8 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
{
CachedPlan *plan;
List *plist;
+ ListCell *cell;
+ Oid planUserId = InvalidOid;
bool snapshot_set;
bool spi_pushed;
MemoryContext plan_context;
@@ -914,6 +927,24 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
PopActiveSnapshot();
/*
+ * Check whether the generated plan assumes a particular user-id, or not.
+ * In case when a valid user-id is recorded on PlannedStmt->planUserId,
+ * it should be kept and used to validation check of the cached plan
+ * under the "current" user-id.
+ */
+ foreach (cell, plist)
+ {
+ PlannedStmt *pstmt = lfirst(cell);
+
+ if (IsA(pstmt, PlannedStmt) && OidIsValid(pstmt->planUserId))
+ {
+ Assert(!OidIsValid(planUserId) || planUserId == pstmt->planUserId);
+
+ planUserId = pstmt->planUserId;
+ }
+ }
+
+ /*
* Normally we make a dedicated memory context for the CachedPlan and its
* subsidiary data. (It's probably not going to be large, but just in
* case, use the default maxsize parameter. It's transient for the
@@ -956,6 +987,7 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
plan->is_oneshot = plansource->is_oneshot;
plan->is_saved = false;
plan->is_valid = true;
+ plan->planUserId = planUserId;
/* assign generation number to new plan */
plan->generation = ++(plansource->generation);
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 66fb63b..828c10e 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -50,6 +50,7 @@
#include "catalog/pg_opclass.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_rewrite.h"
+#include "catalog/pg_rowsecurity.h"
#include "catalog/pg_tablespace.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
@@ -935,6 +936,11 @@ RelationBuildDesc(Oid targetRelId, bool insertIt)
else
relation->trigdesc = NULL;
+ if (relation->rd_rel->relhasrowsecurity)
+ RelationBuildRowSecurity(relation);
+ else
+ relation->rsdesc = NULL;
+
/*
* if it's an index, initialize index-related information
*/
@@ -1842,6 +1848,8 @@ RelationDestroyRelation(Relation relation)
MemoryContextDelete(relation->rd_indexcxt);
if (relation->rd_rulescxt)
MemoryContextDelete(relation->rd_rulescxt);
+ if (relation->rsdesc)
+ MemoryContextDelete(relation->rsdesc->rscxt);
if (relation->rd_fdwroutine)
pfree(relation->rd_fdwroutine);
pfree(relation);
@@ -3161,7 +3169,13 @@ RelationCacheInitializePhase3(void)
relation->rd_rel->relhastriggers = false;
restart = true;
}
-
+ if (relation->rd_rel->relhasrowsecurity && relation->rsdesc == NULL)
+ {
+ RelationBuildRowSecurity(relation);
+ if (relation->rsdesc == NULL)
+ relation->rd_rel->relhasrowsecurity = false;
+ restart = true;
+ }
/* Release hold on the relation */
RelationDecrementReferenceCount(relation);
@@ -4407,6 +4421,7 @@ load_relcache_init_file(bool shared)
rel->rd_rules = NULL;
rel->rd_rulescxt = NULL;
rel->trigdesc = NULL;
+ rel->rsdesc = NULL;
rel->rd_indexprs = NIL;
rel->rd_indpred = NIL;
rel->rd_exclops = NULL;
diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c
index 247ad92..9603ef4 100644
--- a/src/bin/pg_dump/common.c
+++ b/src/bin/pg_dump/common.c
@@ -244,6 +244,10 @@ getSchemaData(Archive *fout, int *numTablesPtr)
write_msg(NULL, "reading rewrite rules\n");
getRules(fout, &numRules);
+ if (g_verbose)
+ write_msg(NULL, "reading row-security policies\n");
+ getRowSecurity(fout, tblinfo, numTables);
+
*numTablesPtr = numTables;
return tblinfo;
}
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 50619a2..c3cb9a0 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3125,6 +3125,7 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, RestoreOptions *ropt, bool isDat
strcmp(te->desc, "INDEX") == 0 ||
strcmp(te->desc, "RULE") == 0 ||
strcmp(te->desc, "TRIGGER") == 0 ||
+ strcmp(te->desc, "ROW SECURITY") == 0 ||
strcmp(te->desc, "USER MAPPING") == 0)
{
/* these object types don't have separate owners */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index e1ef55f..0786393 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -249,6 +249,7 @@ static char *myFormatType(const char *typname, int32 typmod);
static void getBlobs(Archive *fout);
static void dumpBlob(Archive *fout, BlobInfo *binfo);
static int dumpBlobs(Archive *fout, void *arg);
+static void dumpRowSecurity(Archive *fout, RowSecurityInfo *rsinfo);
static void dumpDatabase(Archive *AH);
static void dumpEncoding(Archive *AH);
static void dumpStdStrings(Archive *AH);
@@ -2686,6 +2687,134 @@ dumpBlobs(Archive *fout, void *arg)
return 1;
}
+/*
+ * getRowSecurity
+ * get information about every row-security policy on a dumpable table
+ */
+void
+getRowSecurity(Archive *fout, TableInfo tblinfo[], int numTables)
+{
+ PQExpBuffer query = createPQExpBuffer();
+ PGresult *res;
+ RowSecurityInfo *rsinfo;
+ int i_oid;
+ int i_tableoid;
+ int i_rseccmd;
+ int i_rsecqual;
+ int i, j, ntups;
+
+ /* row-security is not supported prior to v9.4 */
+ if (fout->remoteVersion < 90400)
+ return;
+
+ for (i=0; i < numTables; i++)
+ {
+ TableInfo *tbinfo = &tblinfo[i];
+
+ if (!tbinfo->hasrowsec || !tbinfo->dobj.dump)
+ continue;
+
+ if (g_verbose)
+ write_msg(NULL, "reading row-security policy for table \"%s\"\n",
+ tbinfo->dobj.name);
+
+ /*
+ * select table schema to ensure regproc name is qualified if needed
+ */
+ selectSourceSchema(fout, tbinfo->dobj.namespace->dobj.name);
+
+ resetPQExpBuffer(query);
+
+ appendPQExpBuffer(query,
+ "SELECT oid, tableoid, s.rseccmd, "
+ "pg_get_expr(s.rsecqual, s.rsecrelid) AS rsecqual "
+ "FROM pg_catalog.pg_rowsecurity s "
+ "WHERE rsecrelid = '%u'",
+ tbinfo->dobj.catId.oid);
+ res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+
+ ntups = PQntuples(res);
+
+ i_oid = PQfnumber(res, "oid");
+ i_tableoid = PQfnumber(res, "tableoid");
+ i_rseccmd = PQfnumber(res, "rseccmd");
+ i_rsecqual = PQfnumber(res, "rsecqual");
+
+ rsinfo = pg_malloc(ntups * sizeof(RowSecurityInfo));
+ for (j=0; j < ntups; j++)
+ {
+ char namebuf[NAMEDATALEN + 1];
+
+ rsinfo[j].dobj.objType = DO_ROW_SECURITY;
+ rsinfo[j].dobj.catId.tableoid =
+ atooid(PQgetvalue(res, j, i_tableoid));
+ rsinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
+ AssignDumpId(&rsinfo[j].dobj);
+ snprintf(namebuf, sizeof(namebuf), "row-security of %s",
+ tbinfo->rolname);
+ rsinfo[j].dobj.name = namebuf;
+ rsinfo[j].dobj.namespace = tbinfo->dobj.namespace;
+ rsinfo[j].rstable = tbinfo;
+ rsinfo[j].rseccmd = pg_strdup(PQgetvalue(res, j, i_rseccmd));
+ rsinfo[j].rsecqual = pg_strdup(PQgetvalue(res, j, i_rsecqual));
+ }
+ PQclear(res);
+ }
+ destroyPQExpBuffer(query);
+}
+
+/*
+ * dumpRowSecurity
+ * dump the definition of the given row-security policy
+ */
+static void
+dumpRowSecurity(Archive *fout, RowSecurityInfo *rsinfo)
+{
+ TableInfo *tbinfo = rsinfo->rstable;
+ PQExpBuffer query;
+ PQExpBuffer delqry;
+ const char *cmd;
+
+ if (dataOnly || !tbinfo->hasrowsec)
+ return;
+
+ query = createPQExpBuffer();
+ delqry = createPQExpBuffer();
+ appendPQExpBuffer(query, "ALTER TABLE %s SET ROW SECURITY ",
+ fmtId(tbinfo->dobj.name));
+ appendPQExpBuffer(delqry, "ALTER TABLE %s RESET ROW SECURITY ",
+ fmtId(tbinfo->dobj.name));
+ if (strcmp(rsinfo->rseccmd, "a") == 0)
+ cmd = "ALL";
+ else if (strcmp(rsinfo->rseccmd, "s") == 0)
+ cmd = "SELECT";
+ else if (strcmp(rsinfo->rseccmd, "i") == 0)
+ cmd = "INSERT";
+ else if (strcmp(rsinfo->rseccmd, "u") == 0)
+ cmd = "UPDATE";
+ else if (strcmp(rsinfo->rseccmd, "d") == 0)
+ cmd = "DELETE";
+ else
+ {
+ write_msg(NULL, "unexpected command type: '%s'\n", rsinfo->rseccmd);
+ exit_nicely(1);
+ }
+ appendPQExpBuffer(query, "FOR %s TO %s;\n", cmd, rsinfo->rsecqual);
+ appendPQExpBuffer(delqry, "FOR %s;\n", cmd);
+
+ ArchiveEntry(fout, rsinfo->dobj.catId, rsinfo->dobj.dumpId,
+ rsinfo->dobj.name,
+ rsinfo->dobj.namespace->dobj.name,
+ NULL,
+ tbinfo->rolname, false,
+ "ROW SECURITY", SECTION_POST_DATA,
+ query->data, delqry->data, NULL,
+ NULL, 0,
+ NULL, NULL);
+
+ destroyPQExpBuffer(query);
+}
+
static void
binary_upgrade_set_type_oids_by_type_oid(Archive *fout,
PQExpBuffer upgrade_buffer,
@@ -4214,6 +4343,7 @@ getTables(Archive *fout, int *numTables)
int i_relhastriggers;
int i_relhasindex;
int i_relhasrules;
+ int i_relhasrowsec;
int i_relhasoids;
int i_relfrozenxid;
int i_toastoid;
@@ -4252,7 +4382,45 @@ getTables(Archive *fout, int *numTables)
* we cannot correctly identify inherited columns, owned sequences, etc.
*/
- if (fout->remoteVersion >= 90300)
+ if (fout->remoteVersion >= 90400)
+ {
+ /*
+ * Left join to pick up dependency info linking sequences to their
+ * owning column, if any (note this dependency is AUTO as of 8.2)
+ */
+ appendPQExpBuffer(query,
+ "SELECT c.tableoid, c.oid, c.relname, "
+ "c.relacl, c.relkind, c.relnamespace, "
+ "(%s c.relowner) AS rolname, "
+ "c.relchecks, c.relhastriggers, "
+ "c.relhasindex, c.relhasrules, c.relhasoids, "
+ "c.relhasrowsecurity, "
+ "c.relfrozenxid, tc.oid AS toid, "
+ "tc.relfrozenxid AS tfrozenxid, "
+ "c.relpersistence, c.relispopulated, "
+ "c.relpages, "
+ "CASE WHEN c.reloftype <> 0 THEN c.reloftype::pg_catalog.regtype ELSE NULL END AS reloftype, "
+ "d.refobjid AS owning_tab, "
+ "d.refobjsubid AS owning_col, "
+ "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
+ "array_to_string(c.reloptions, ', ') AS reloptions, "
+ "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions "
+ "FROM pg_class c "
+ "LEFT JOIN pg_depend d ON "
+ "(c.relkind = '%c' AND "
+ "d.classid = c.tableoid AND d.objid = c.oid AND "
+ "d.objsubid = 0 AND "
+ "d.refclassid = c.tableoid AND d.deptype = 'a') "
+ "LEFT JOIN pg_class tc ON (c.reltoastrelid = tc.oid) "
+ "WHERE c.relkind in ('%c', '%c', '%c', '%c', '%c', '%c') "
+ "ORDER BY c.oid",
+ username_subquery,
+ RELKIND_SEQUENCE,
+ RELKIND_RELATION, RELKIND_SEQUENCE,
+ RELKIND_VIEW, RELKIND_COMPOSITE_TYPE,
+ RELKIND_MATVIEW, RELKIND_FOREIGN_TABLE);
+ }
+ else if (fout->remoteVersion >= 90300)
{
/*
* Left join to pick up dependency info linking sequences to their
@@ -4264,6 +4432,7 @@ getTables(Archive *fout, int *numTables)
"(%s c.relowner) AS rolname, "
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"c.relfrozenxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"c.relpersistence, c.relispopulated, "
@@ -4303,6 +4472,7 @@ getTables(Archive *fout, int *numTables)
"(%s c.relowner) AS rolname, "
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"c.relfrozenxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"c.relpersistence, 't' as relispopulated, "
@@ -4340,6 +4510,7 @@ getTables(Archive *fout, int *numTables)
"(%s c.relowner) AS rolname, "
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"c.relfrozenxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"'p' AS relpersistence, 't' as relispopulated, "
@@ -4376,6 +4547,7 @@ getTables(Archive *fout, int *numTables)
"(%s c.relowner) AS rolname, "
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"c.relfrozenxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"'p' AS relpersistence, 't' as relispopulated, "
@@ -4412,6 +4584,7 @@ getTables(Archive *fout, int *numTables)
"(%s c.relowner) AS rolname, "
"c.relchecks, (c.reltriggers <> 0) AS relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"c.relfrozenxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"'p' AS relpersistence, 't' as relispopulated, "
@@ -4448,6 +4621,7 @@ getTables(Archive *fout, int *numTables)
"(%s relowner) AS rolname, "
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"0 AS relfrozenxid, "
"0 AS toid, "
"0 AS tfrozenxid, "
@@ -4484,6 +4658,7 @@ getTables(Archive *fout, int *numTables)
"(%s relowner) AS rolname, "
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"0 AS relfrozenxid, "
"0 AS toid, "
"0 AS tfrozenxid, "
@@ -4516,6 +4691,7 @@ getTables(Archive *fout, int *numTables)
"(%s relowner) AS rolname, "
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"0 AS relfrozenxid, "
"0 AS toid, "
"0 AS tfrozenxid, "
@@ -4543,6 +4719,7 @@ getTables(Archive *fout, int *numTables)
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, "
"'t'::bool AS relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"0 AS relfrozenxid, "
"0 AS toid, "
"0 AS tfrozenxid, "
@@ -4580,6 +4757,7 @@ getTables(Archive *fout, int *numTables)
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, "
"'t'::bool AS relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"0 as relfrozenxid, "
"0 AS toid, "
"0 AS tfrozenxid, "
@@ -4627,6 +4805,7 @@ getTables(Archive *fout, int *numTables)
i_relhastriggers = PQfnumber(res, "relhastriggers");
i_relhasindex = PQfnumber(res, "relhasindex");
i_relhasrules = PQfnumber(res, "relhasrules");
+ i_relhasrowsec = PQfnumber(res, "relhasrowsecurity");
i_relhasoids = PQfnumber(res, "relhasoids");
i_relfrozenxid = PQfnumber(res, "relfrozenxid");
i_toastoid = PQfnumber(res, "toid");
@@ -4675,6 +4854,7 @@ getTables(Archive *fout, int *numTables)
tblinfo[i].hasindex = (strcmp(PQgetvalue(res, i, i_relhasindex), "t") == 0);
tblinfo[i].hasrules = (strcmp(PQgetvalue(res, i, i_relhasrules), "t") == 0);
tblinfo[i].hastriggers = (strcmp(PQgetvalue(res, i, i_relhastriggers), "t") == 0);
+ tblinfo[i].hasrowsec = (strcmp(PQgetvalue(res, i, i_relhasrowsec), "t") == 0);
tblinfo[i].hasoids = (strcmp(PQgetvalue(res, i, i_relhasoids), "t") == 0);
tblinfo[i].relispopulated = (strcmp(PQgetvalue(res, i, i_relispopulated), "t") == 0);
tblinfo[i].relpages = atoi(PQgetvalue(res, i, i_relpages));
@@ -7758,6 +7938,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
NULL, 0,
dumpBlobs, NULL);
break;
+ case DO_ROW_SECURITY:
+ dumpRowSecurity(fout, (RowSecurityInfo *) dobj);
+ break;
case DO_PRE_DATA_BOUNDARY:
case DO_POST_DATA_BOUNDARY:
/* never dumped, nothing to do */
@@ -14935,6 +15118,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
case DO_TRIGGER:
case DO_EVENT_TRIGGER:
case DO_DEFAULT_ACL:
+ case DO_ROW_SECURITY:
/* Post-data objects: must come after the post-data boundary */
addObjectDependency(dobj, postDataBound->dumpId);
break;
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 2c5971c..4dc00db 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -111,7 +111,8 @@ typedef enum
DO_PRE_DATA_BOUNDARY,
DO_POST_DATA_BOUNDARY,
DO_EVENT_TRIGGER,
- DO_REFRESH_MATVIEW
+ DO_REFRESH_MATVIEW,
+ DO_ROW_SECURITY,
} DumpableObjectType;
typedef struct _dumpableObject
@@ -244,6 +245,7 @@ typedef struct _tableInfo
bool hasindex; /* does it have any indexes? */
bool hasrules; /* does it have any rules? */
bool hastriggers; /* does it have any triggers? */
+ bool hasrowsec; /* does it have any row-security policy? */
bool hasoids; /* does it have OIDs? */
uint32 frozenxid; /* for restore frozen xid */
Oid toast_oid; /* for restore toast frozen xid */
@@ -482,6 +484,14 @@ typedef struct _blobInfo
char *blobacl;
} BlobInfo;
+typedef struct _rowSecurityInfo
+{
+ DumpableObject dobj;
+ TableInfo *rstable;
+ char *rseccmd;
+ char *rsecqual;
+} RowSecurityInfo;
+
/* global decls */
extern bool force_quotes; /* double-quotes for identifiers flag */
extern bool g_verbose; /* verbose flag */
@@ -573,5 +583,6 @@ extern DefaultACLInfo *getDefaultACLs(Archive *fout, int *numDefaultACLs);
extern void getExtensionMembership(Archive *fout, ExtensionInfo extinfo[],
int numExtensions);
extern EventTriggerInfo *getEventTriggers(Archive *fout, int *numEventTriggers);
+extern void getRowSecurity(Archive *fout, TableInfo tblinfo[], int numTables);
#endif /* PG_DUMP_H */
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 141e713..5c5777c 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -1342,6 +1342,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
"BLOB DATA (ID %d)",
obj->dumpId);
return;
+ case DO_ROW_SECURITY:
+ snprintf(buf, bufsize,
+ "ROW-SECURITY POLICY (ID %d OID %u)",
+ obj->dumpId, obj->catId.oid);
+ return;
case DO_PRE_DATA_BOUNDARY:
snprintf(buf, bufsize,
"PRE-DATA BOUNDARY (ID %d)",
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index dad1d5a..de09410 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2697,6 +2697,10 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys
appendPQExpBuffer(&buf,
",\n pg_catalog.obj_description(c.oid, 'pg_class') as \"%s\"",
gettext_noop("Description"));
+ if (pset.sversion >= 90300)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_get_expr(rs.rsecqual, c.oid) as \"%s\"",
+ gettext_noop("Row-security"));
}
appendPQExpBuffer(&buf,
@@ -2706,6 +2710,9 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys
appendPQExpBuffer(&buf,
"\n LEFT JOIN pg_catalog.pg_index i ON i.indexrelid = c.oid"
"\n LEFT JOIN pg_catalog.pg_class c2 ON i.indrelid = c2.oid");
+ if (verbose && pset.sversion >= 90300)
+ appendPQExpBuffer(&buf,
+ "\n LEFT JOIN pg_rowsecurity rs ON rs.rsecrelid = c.oid");
appendPQExpBuffer(&buf, "\nWHERE c.relkind IN (");
if (showTables)
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index 3aefbb5e..c96e8d4 100644
--- a/src/include/catalog/dependency.h
+++ b/src/include/catalog/dependency.h
@@ -147,6 +147,7 @@ typedef enum ObjectClass
OCLASS_DEFACL, /* pg_default_acl */
OCLASS_EXTENSION, /* pg_extension */
OCLASS_EVENT_TRIGGER, /* pg_event_trigger */
+ OCLASS_ROWSECURITY, /* pg_rowsecurity */
MAX_OCLASS /* MUST BE LAST */
} ObjectClass;
diff --git a/src/include/catalog/indexing.h b/src/include/catalog/indexing.h
index 4860e98..f45e42b 100644
--- a/src/include/catalog/indexing.h
+++ b/src/include/catalog/indexing.h
@@ -313,6 +313,11 @@ DECLARE_UNIQUE_INDEX(pg_extension_name_index, 3081, on pg_extension using btree(
DECLARE_UNIQUE_INDEX(pg_range_rngtypid_index, 3542, on pg_range using btree(rngtypid oid_ops));
#define RangeTypidIndexId 3542
+DECLARE_UNIQUE_INDEX(pg_rowsecurity_oid_index, 3819, on pg_rowsecurity using btree(oid oid_ops));
+#define RowSecurityOidIndexId 3819
+DECLARE_UNIQUE_INDEX(pg_rowsecurity_relid_index, 3839, on pg_rowsecurity using btree(rsecrelid oid_ops, rseccmd char_ops));
+#define RowSecurityRelidIndexId 3839
+
/* last step of initialization script: build the indexes declared above */
BUILD_INDICES
diff --git a/src/include/catalog/pg_class.h b/src/include/catalog/pg_class.h
index 49c4f6f..6a16819 100644
--- a/src/include/catalog/pg_class.h
+++ b/src/include/catalog/pg_class.h
@@ -64,6 +64,7 @@ CATALOG(pg_class,1259) BKI_BOOTSTRAP BKI_ROWTYPE_OID(83) BKI_SCHEMA_MACRO
bool relhaspkey; /* has (or has had) PRIMARY KEY index */
bool relhasrules; /* has (or has had) any rules */
bool relhastriggers; /* has (or has had) any TRIGGERs */
+ bool relhasrowsecurity; /* has (or has had) row-security policy */
bool relhassubclass; /* has (or has had) derived classes */
bool relispopulated; /* matview currently holds query results */
TransactionId relfrozenxid; /* all Xids < this are frozen in this rel */
@@ -93,7 +94,7 @@ typedef FormData_pg_class *Form_pg_class;
* ----------------
*/
-#define Natts_pg_class 28
+#define Natts_pg_class 29
#define Anum_pg_class_relname 1
#define Anum_pg_class_relnamespace 2
#define Anum_pg_class_reltype 3
@@ -116,12 +117,13 @@ typedef FormData_pg_class *Form_pg_class;
#define Anum_pg_class_relhaspkey 20
#define Anum_pg_class_relhasrules 21
#define Anum_pg_class_relhastriggers 22
-#define Anum_pg_class_relhassubclass 23
-#define Anum_pg_class_relispopulated 24
-#define Anum_pg_class_relfrozenxid 25
-#define Anum_pg_class_relminmxid 26
-#define Anum_pg_class_relacl 27
-#define Anum_pg_class_reloptions 28
+#define Anum_pg_class_relhasrowsecurity 23
+#define Anum_pg_class_relhassubclass 24
+#define Anum_pg_class_relispopulated 25
+#define Anum_pg_class_relfrozenxid 26
+#define Anum_pg_class_relminmxid 27
+#define Anum_pg_class_relacl 28
+#define Anum_pg_class_reloptions 29
/* ----------------
* initial contents of pg_class
@@ -136,13 +138,13 @@ typedef FormData_pg_class *Form_pg_class;
* Note: "3" in the relfrozenxid column stands for FirstNormalTransactionId;
* similarly, "1" in relminmxid stands for FirstMultiXactId
*/
-DATA(insert OID = 1247 ( pg_type PGNSP 71 0 PGUID 0 0 0 0 0 0 0 f f p r 30 0 t f f f f t 3 1 _null_ _null_ ));
+DATA(insert OID = 1247 ( pg_type PGNSP 71 0 PGUID 0 0 0 0 0 0 0 f f p r 30 0 t f f f f f t 3 1 _null_ _null_ ));
DESCR("");
-DATA(insert OID = 1249 ( pg_attribute PGNSP 75 0 PGUID 0 0 0 0 0 0 0 f f p r 21 0 f f f f f t 3 1 _null_ _null_ ));
+DATA(insert OID = 1249 ( pg_attribute PGNSP 75 0 PGUID 0 0 0 0 0 0 0 f f p r 21 0 f f f f f f t 3 1 _null_ _null_ ));
DESCR("");
-DATA(insert OID = 1255 ( pg_proc PGNSP 81 0 PGUID 0 0 0 0 0 0 0 f f p r 27 0 t f f f f t 3 1 _null_ _null_ ));
+DATA(insert OID = 1255 ( pg_proc PGNSP 81 0 PGUID 0 0 0 0 0 0 0 f f p r 27 0 t f f f f f t 3 1 _null_ _null_ ));
DESCR("");
-DATA(insert OID = 1259 ( pg_class PGNSP 83 0 PGUID 0 0 0 0 0 0 0 f f p r 28 0 t f f f f t 3 1 _null_ _null_ ));
+DATA(insert OID = 1259 ( pg_class PGNSP 83 0 PGUID 0 0 0 0 0 0 0 f f p r 29 0 t f f f f f t 3 1 _null_ _null_ ));
DESCR("");
diff --git a/src/include/catalog/pg_rowsecurity.h b/src/include/catalog/pg_rowsecurity.h
new file mode 100644
index 0000000..a068b35
--- /dev/null
+++ b/src/include/catalog/pg_rowsecurity.h
@@ -0,0 +1,76 @@
+/*
+ * pg_rowsecurity.h
+ * definition of the system catalog for row-security policy (pg_rowsecurity)
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ */
+#ifndef PG_ROWSECURITY_H
+#define PG_ROWSECURITY_H
+
+#include "catalog/genbki.h"
+#include "nodes/primnodes.h"
+#include "utils/memutils.h"
+#include "utils/relcache.h"
+
+/* ----------------
+ * pg_rowlevelsec definition. cpp turns this into
+ * typedef struct FormData_pg_rowlevelsec
+ * ----------------
+ */
+#define RowSecurityRelationId 3838
+
+CATALOG(pg_rowsecurity,3838)
+{
+ /* Oid of the relation that has row-security policy */
+ Oid rsecrelid;
+
+ /* One of ROWSECURITY_CMD_* below */
+ char rseccmd;
+#ifdef CATALOG_VARLEN
+ pg_node_tree rsecqual;
+#endif
+} FormData_pg_rowsecurity;
+
+/* ----------------
+ * Form_pg_rowlevelsec corresponds to a pointer to a row with
+ * the format of pg_rowlevelsec relation.
+ * ----------------
+ */
+typedef FormData_pg_rowsecurity *Form_pg_rowsecurity;
+
+/* ----------------
+ * compiler constants for pg_rowlevelsec
+ * ----------------
+ */
+#define Natts_pg_rowsecurity 3
+#define Anum_pg_rowsecurity_rsecrelid 1
+#define Anum_pg_rowsecurity_rseccmd 2
+#define Anum_pg_rowsecurity_rsecqual 3
+
+#define ROWSECURITY_CMD_ALL 'a'
+#define ROWSECURITY_CMD_SELECT 's'
+#define ROWSECURITY_CMD_INSERT 'i'
+#define ROWSECURITY_CMD_UPDATE 'u'
+#define ROWSECURITY_CMD_DELETE 'd'
+
+typedef struct
+{
+ Oid rsecid;
+ Expr *qual;
+ bool hassublinks;
+} RowSecurityEntry;
+
+typedef struct
+{
+ MemoryContext rscxt;
+ RowSecurityEntry rsall; /* row-security policy for ALL */
+} RowSecurityDesc;
+
+extern void RelationBuildRowSecurity(Relation relation);
+extern void ATExecSetRowSecurity(Relation relation,
+ const char *cmdname, Node *clause);
+extern void RemoveRowSecurityById(Oid relationId);
+
+#endif /* PG_ROWSECURITY_H */
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index edced29..bd14531 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -279,6 +279,7 @@ extern int trace_recovery(int trace_level);
/* flags to be OR'd to form sec_context */
#define SECURITY_LOCAL_USERID_CHANGE 0x0001
#define SECURITY_RESTRICTED_OPERATION 0x0002
+#define SECURITY_ROW_LEVEL_DISABLED 0x0004
extern char *DatabasePath;
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 3b430e0..5f8b7f7 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -308,6 +308,8 @@ typedef struct JunkFilter
* ConstraintExprs array of constraint-checking expr states
* junkFilter for removing junk attributes from tuples
* projectReturning for computing a RETURNING list
+ * rowSecurity for row-security checks
+ * rowSecParams param-list if row-security has SubLink
* ----------------
*/
typedef struct ResultRelInfo
@@ -329,6 +331,8 @@ typedef struct ResultRelInfo
List **ri_ConstraintExprs;
JunkFilter *ri_junkFilter;
ProjectionInfo *ri_projectReturning;
+ Node *ri_rowSecurity;
+ List *ri_rowSecParams;
} ResultRelInfo;
/* ----------------
diff --git a/src/include/nodes/nodeFuncs.h b/src/include/nodes/nodeFuncs.h
index fe7cfd3..23c3553 100644
--- a/src/include/nodes/nodeFuncs.h
+++ b/src/include/nodes/nodeFuncs.h
@@ -24,6 +24,7 @@
#define QTW_IGNORE_RANGE_TABLE 0x08 /* skip rangetable entirely */
#define QTW_EXAMINE_RTES 0x10 /* examine RTEs */
#define QTW_DONT_COPY_QUERY 0x20 /* do not copy top Query */
+#define QTW_IGNORE_RETURNING 0x40 /* skip returning clause */
extern Oid exprType(const Node *expr);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 51fef68..72ca771 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -31,7 +31,8 @@ typedef enum QuerySource
QSRC_PARSER, /* added by parse analysis (now unused) */
QSRC_INSTEAD_RULE, /* added by unconditional INSTEAD rule */
QSRC_QUAL_INSTEAD_RULE, /* added by conditional INSTEAD rule */
- QSRC_NON_INSTEAD_RULE /* added by non-INSTEAD rule */
+ QSRC_NON_INSTEAD_RULE, /* added by non-INSTEAD rule */
+ QSRC_ROW_SECURITY, /* added by row-security */
} QuerySource;
/* Sort ordering options for ORDER BY and CREATE INDEX */
@@ -112,7 +113,9 @@ typedef struct Query
int resultRelation; /* rtable index of target relation for
* INSERT/UPDATE/DELETE; 0 for SELECT */
-
+ int sourceRelation; /* rtable index of source relation for
+ * UPDATE/DELETE, if not identical with
+ * resultRelation; 0 for elsewhere */
bool hasAggs; /* has aggregates in tlist or havingQual */
bool hasWindowFuncs; /* has window functions in tlist */
bool hasSubLinks; /* has subquery SubLink */
@@ -733,6 +736,11 @@ typedef struct RangeTblEntry
*/
Query *subquery; /* the sub-query */
bool security_barrier; /* is from security_barrier view? */
+ Oid rowsec_relid; /* OID of the original relation, if this
+ * sub-query originated from row-security
+ * policy on the relation. Elsewhere, it
+ * should be InvalidOid.
+ */
/*
* Fields valid for a join RTE (else NULL/zero):
@@ -1284,6 +1292,8 @@ typedef enum AlterTableType
AT_DropInherit, /* NO INHERIT parent */
AT_AddOf, /* OF <type_name> */
AT_DropOf, /* NOT OF */
+ AT_SetRowSecurity, /* SET ROW SECURITY (...) */
+ AT_ResetRowSecurity, /* RESET ROW SECURITY */
AT_GenericOptions /* OPTIONS (...) */
} AlterTableType;
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 44ea0b7..889dc7d 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -67,6 +67,8 @@ typedef struct PlannedStmt
List *invalItems; /* other dependencies, as PlanInvalItems */
int nParamExec; /* number of PARAM_EXEC Params used */
+
+ Oid planUserId; /* user-id this plan assumed, or InvalidOid */
} PlannedStmt;
/* macro for fetching the Plan associated with a SubPlan node */
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index a2853fb..e7eb8cf 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -98,6 +98,8 @@ typedef struct PlannerGlobal
Index lastRowMarkId; /* highest PlanRowMark ID assigned */
bool transientPlan; /* redo plan when TransactionXmin changes? */
+
+ Oid planUserId; /* User-Id to be assumed on this plan */
} PlannerGlobal;
/* macro for fetching the Plan associated with a SubPlan node */
@@ -1426,6 +1428,10 @@ typedef struct AppendRelInfo
*/
Index parent_relid; /* RT index of append parent rel */
Index child_relid; /* RT index of append child rel */
+ Index child_result; /* RT index of append child rel's source,
+ * if source of result relation is not
+ * identical. Elsewhere, 0.
+ */
/*
* For an inheritance appendrel, the parent and child are both regular
diff --git a/src/include/optimizer/rowsecurity.h b/src/include/optimizer/rowsecurity.h
new file mode 100644
index 0000000..ff4dd9a
--- /dev/null
+++ b/src/include/optimizer/rowsecurity.h
@@ -0,0 +1,27 @@
+/* -------------------------------------------------------------------------
+ *
+ * rowsecurity.h
+ * prototypes for optimizer/rowsecurity.c
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * -------------------------------------------------------------------------
+ */
+#ifndef ROWSECURITY_H
+#define ROWSECURITY_H
+
+#include "nodes/execnodes.h"
+#include "nodes/parsenodes.h"
+#include "nodes/relation.h"
+#include "utils/rel.h"
+
+typedef List *(*row_security_policy_hook_type)(CmdType cmdtype,
+ Relation relation);
+extern PGDLLIMPORT row_security_policy_hook_type row_security_policy_hook;
+
+extern bool copy_row_security_policy(CopyStmt *stmt,
+ Relation relation, List *attnums);
+extern void apply_row_security_policy(PlannerInfo *root);
+
+#endif /* ROWSECURITY_H */
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index bea3b07..3910853 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -63,7 +63,8 @@ typedef enum ParseExprKind
EXPR_KIND_INDEX_PREDICATE, /* index predicate */
EXPR_KIND_ALTER_COL_TRANSFORM, /* transform expr in ALTER COLUMN TYPE */
EXPR_KIND_EXECUTE_PARAMETER, /* parameter value in EXECUTE */
- EXPR_KIND_TRIGGER_WHEN /* WHEN condition in CREATE TRIGGER */
+ EXPR_KIND_TRIGGER_WHEN, /* WHEN condition in CREATE TRIGGER */
+ EXPR_KIND_ROW_SECURITY, /* ROW SECURITY policy for a table */
} ParseExprKind;
diff --git a/src/include/rewrite/rewriteHandler.h b/src/include/rewrite/rewriteHandler.h
index f0604b0..6b6c364 100644
--- a/src/include/rewrite/rewriteHandler.h
+++ b/src/include/rewrite/rewriteHandler.h
@@ -18,6 +18,7 @@
#include "nodes/parsenodes.h"
extern List *QueryRewrite(Query *parsetree);
+extern void QueryRewriteExpr(Node *node, List *activeRIRs);
extern void AcquireRewriteLocks(Query *parsetree, bool forUpdatePushedDown);
extern Node *build_column_default(Relation rel, int attrno);
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index 72f8491..35819fa 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -128,6 +128,8 @@ typedef struct CachedPlan
bool is_oneshot; /* is it a "oneshot" plan? */
bool is_saved; /* is CachedPlan in a long-lived context? */
bool is_valid; /* is the stmt_list currently valid? */
+ Oid planUserId; /* is user-id that is assumed on this cached
+ plan, or InvalidOid if portable for anybody */
TransactionId saved_xmin; /* if valid, replan when TransactionXmin
* changes from this value */
int generation; /* parent's generation number for this plan */
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 589c9a8..4e4ac65 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -18,6 +18,7 @@
#include "catalog/pg_am.h"
#include "catalog/pg_class.h"
#include "catalog/pg_index.h"
+#include "catalog/pg_rowsecurity.h"
#include "fmgr.h"
#include "nodes/bitmapset.h"
#include "rewrite/prs2lock.h"
@@ -109,6 +110,7 @@ typedef struct RelationData
RuleLock *rd_rules; /* rewrite rules */
MemoryContext rd_rulescxt; /* private memory cxt for rd_rules, if any */
TriggerDesc *trigdesc; /* Trigger info, or NULL if rel has none */
+ RowSecurityDesc *rsdesc; /* Row-security policy, or NULL */
/*
* rd_options is set whenever rd_rel is loaded into the relcache entry.
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
new file mode 100644
index 0000000..04591df
--- /dev/null
+++ b/src/test/regress/expected/rowsecurity.out
@@ -0,0 +1,950 @@
+--
+-- Test of Row-level security feature
+--
+-- Clean up in case a prior regression run failed
+-- Suppress NOTICE messages when users/groups don't exist
+SET client_min_messages TO 'warning';
+DROP USER IF EXISTS rls_regress_user0;
+DROP USER IF EXISTS rls_regress_user1;
+DROP USER IF EXISTS rls_regress_user2;
+DROP SCHEMA IF EXISTS rls_regress_schema CASCADE;
+RESET client_min_messages;
+-- initial setup
+CREATE USER rls_regress_user0;
+CREATE USER rls_regress_user1;
+CREATE USER rls_regress_user2;
+CREATE SCHEMA rls_regress_schema;
+GRANT ALL ON SCHEMA rls_regress_schema TO public;
+SET search_path = rls_regress_schema;
+-- setup of malicious function
+CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool
+ COST 0.0000001 LANGUAGE plpgsql
+ AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END';
+GRANT EXECUTE ON FUNCTION f_leak(text) TO public;
+-- BASIC Row-Level Security Scenario
+SET SESSION AUTHORIZATION rls_regress_user0;
+CREATE TABLE uaccount (
+ pguser name primary key,
+ seclv int
+);
+INSERT INTO uaccount VALUES
+ ('rls_regress_user0', 99),
+ ('rls_regress_user1', 1),
+ ('rls_regress_user2', 2),
+ ('rls_regress_user3', 3);
+GRANT SELECT ON uaccount TO public;
+CREATE TABLE category (
+ cid int primary key,
+ cname text
+);
+GRANT ALL ON category TO public;
+INSERT INTO category VALUES
+ (11, 'novel'),
+ (22, 'science fiction'),
+ (33, 'technology'),
+ (44, 'manga');
+CREATE TABLE document (
+ did int primary key,
+ cid int references category(cid),
+ dlevel int not null,
+ dauthor name,
+ dtitle text
+);
+GRANT ALL ON document TO public;
+INSERT INTO document VALUES
+ ( 1, 11, 1, 'rls_regress_user1', 'my first novel'),
+ ( 2, 11, 2, 'rls_regress_user1', 'my second novel'),
+ ( 3, 22, 2, 'rls_regress_user1', 'my science fiction'),
+ ( 4, 44, 1, 'rls_regress_user1', 'my first manga'),
+ ( 5, 44, 2, 'rls_regress_user1', 'my second manga'),
+ ( 6, 22, 1, 'rls_regress_user2', 'great science fiction'),
+ ( 7, 33, 2, 'rls_regress_user2', 'great technology book'),
+ ( 8, 44, 1, 'rls_regress_user2', 'great manga');
+-- user's security level must higher than or equal to document's one
+ALTER TABLE document SET ROW SECURITY FOR ALL
+ TO (dlevel <= (SELECT seclv FROM uaccount WHERE pguser = current_user));
+-- viewpoint from rls_regress_user1
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE: f_leak => my first novel
+NOTICE: f_leak => my first manga
+NOTICE: f_leak => great science fiction
+NOTICE: f_leak => great manga
+ did | cid | dlevel | dauthor | dtitle
+-----+-----+--------+-------------------+-----------------------
+ 1 | 11 | 1 | rls_regress_user1 | my first novel
+ 4 | 44 | 1 | rls_regress_user1 | my first manga
+ 6 | 22 | 1 | rls_regress_user2 | great science fiction
+ 8 | 44 | 1 | rls_regress_user2 | great manga
+(4 rows)
+
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+NOTICE: f_leak => my first novel
+NOTICE: f_leak => my first manga
+NOTICE: f_leak => great science fiction
+NOTICE: f_leak => great manga
+ cid | did | dlevel | dauthor | dtitle | cname
+-----+-----+--------+-------------------+-----------------------+-----------------
+ 11 | 1 | 1 | rls_regress_user1 | my first novel | novel
+ 22 | 6 | 1 | rls_regress_user2 | great science fiction | science fiction
+ 44 | 8 | 1 | rls_regress_user2 | great manga | manga
+ 44 | 4 | 1 | rls_regress_user1 | my first manga | manga
+(4 rows)
+
+-- viewpoint from rls_regress_user2
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE: f_leak => my first novel
+NOTICE: f_leak => my second novel
+NOTICE: f_leak => my science fiction
+NOTICE: f_leak => my first manga
+NOTICE: f_leak => my second manga
+NOTICE: f_leak => great science fiction
+NOTICE: f_leak => great technology book
+NOTICE: f_leak => great manga
+ did | cid | dlevel | dauthor | dtitle
+-----+-----+--------+-------------------+-----------------------
+ 1 | 11 | 1 | rls_regress_user1 | my first novel
+ 2 | 11 | 2 | rls_regress_user1 | my second novel
+ 3 | 22 | 2 | rls_regress_user1 | my science fiction
+ 4 | 44 | 1 | rls_regress_user1 | my first manga
+ 5 | 44 | 2 | rls_regress_user1 | my second manga
+ 6 | 22 | 1 | rls_regress_user2 | great science fiction
+ 7 | 33 | 2 | rls_regress_user2 | great technology book
+ 8 | 44 | 1 | rls_regress_user2 | great manga
+(8 rows)
+
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+NOTICE: f_leak => my first novel
+NOTICE: f_leak => my second novel
+NOTICE: f_leak => my science fiction
+NOTICE: f_leak => my first manga
+NOTICE: f_leak => my second manga
+NOTICE: f_leak => great science fiction
+NOTICE: f_leak => great technology book
+NOTICE: f_leak => great manga
+ cid | did | dlevel | dauthor | dtitle | cname
+-----+-----+--------+-------------------+-----------------------+-----------------
+ 11 | 2 | 2 | rls_regress_user1 | my second novel | novel
+ 11 | 1 | 1 | rls_regress_user1 | my first novel | novel
+ 22 | 6 | 1 | rls_regress_user2 | great science fiction | science fiction
+ 22 | 3 | 2 | rls_regress_user1 | my science fiction | science fiction
+ 33 | 7 | 2 | rls_regress_user2 | great technology book | technology
+ 44 | 8 | 1 | rls_regress_user2 | great manga | manga
+ 44 | 5 | 2 | rls_regress_user1 | my second manga | manga
+ 44 | 4 | 1 | rls_regress_user1 | my first manga | manga
+(8 rows)
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+ QUERY PLAN
+----------------------------------------------------------
+ Subquery Scan on document
+ Filter: f_leak(document.dtitle)
+ -> Seq Scan on document document_1
+ Filter: (dlevel <= $0)
+ InitPlan 1 (returns $0)
+ -> Index Scan using uaccount_pkey on uaccount
+ Index Cond: (pguser = "current_user"())
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+ QUERY PLAN
+----------------------------------------------------------------------
+ Hash Join
+ Hash Cond: (category.cid = document.cid)
+ -> Seq Scan on category
+ -> Hash
+ -> Subquery Scan on document
+ Filter: f_leak(document.dtitle)
+ -> Seq Scan on document document_1
+ Filter: (dlevel <= $0)
+ InitPlan 1 (returns $0)
+ -> Index Scan using uaccount_pkey on uaccount
+ Index Cond: (pguser = "current_user"())
+(11 rows)
+
+-- only owner can change row-level security
+ALTER TABLE document SET ROW SECURITY FOR ALL TO (true); -- fail
+ERROR: must be owner of relation document
+ALTER TABLE document RESET ROW SECURITY FOR ALL; -- fail
+ERROR: must be owner of relation document
+SET SESSION AUTHORIZATION rls_regress_user0;
+ALTER TABLE document SET ROW SECURITY FOR ALL TO (dauthor = current_user);
+-- viewpoint from rls_regress_user1 again
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE: f_leak => my first novel
+NOTICE: f_leak => my second novel
+NOTICE: f_leak => my science fiction
+NOTICE: f_leak => my first manga
+NOTICE: f_leak => my second manga
+ did | cid | dlevel | dauthor | dtitle
+-----+-----+--------+-------------------+--------------------
+ 1 | 11 | 1 | rls_regress_user1 | my first novel
+ 2 | 11 | 2 | rls_regress_user1 | my second novel
+ 3 | 22 | 2 | rls_regress_user1 | my science fiction
+ 4 | 44 | 1 | rls_regress_user1 | my first manga
+ 5 | 44 | 2 | rls_regress_user1 | my second manga
+(5 rows)
+
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+NOTICE: f_leak => my first novel
+NOTICE: f_leak => my second novel
+NOTICE: f_leak => my science fiction
+NOTICE: f_leak => my first manga
+NOTICE: f_leak => my second manga
+ cid | did | dlevel | dauthor | dtitle | cname
+-----+-----+--------+-------------------+--------------------+-----------------
+ 11 | 1 | 1 | rls_regress_user1 | my first novel | novel
+ 11 | 2 | 2 | rls_regress_user1 | my second novel | novel
+ 22 | 3 | 2 | rls_regress_user1 | my science fiction | science fiction
+ 44 | 4 | 1 | rls_regress_user1 | my first manga | manga
+ 44 | 5 | 2 | rls_regress_user1 | my second manga | manga
+(5 rows)
+
+-- viewpoint from rls_regress_user2 again
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE: f_leak => great science fiction
+NOTICE: f_leak => great technology book
+NOTICE: f_leak => great manga
+ did | cid | dlevel | dauthor | dtitle
+-----+-----+--------+-------------------+-----------------------
+ 6 | 22 | 1 | rls_regress_user2 | great science fiction
+ 7 | 33 | 2 | rls_regress_user2 | great technology book
+ 8 | 44 | 1 | rls_regress_user2 | great manga
+(3 rows)
+
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+NOTICE: f_leak => great science fiction
+NOTICE: f_leak => great technology book
+NOTICE: f_leak => great manga
+ cid | did | dlevel | dauthor | dtitle | cname
+-----+-----+--------+-------------------+-----------------------+-----------------
+ 22 | 6 | 1 | rls_regress_user2 | great science fiction | science fiction
+ 33 | 7 | 2 | rls_regress_user2 | great technology book | technology
+ 44 | 8 | 1 | rls_regress_user2 | great manga | manga
+(3 rows)
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+ QUERY PLAN
+----------------------------------------------
+ Subquery Scan on document
+ Filter: f_leak(document.dtitle)
+ -> Seq Scan on document document_1
+ Filter: (dauthor = "current_user"())
+(4 rows)
+
+EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+ QUERY PLAN
+----------------------------------------------------
+ Nested Loop
+ -> Subquery Scan on document
+ Filter: f_leak(document.dtitle)
+ -> Seq Scan on document document_1
+ Filter: (dauthor = "current_user"())
+ -> Index Scan using category_pkey on category
+ Index Cond: (cid = document.cid)
+(7 rows)
+
+-- interaction of FK/PK constraints
+SET SESSION AUTHORIZATION rls_regress_user0;
+ALTER TABLE category SET ROW SECURITY FOR ALL
+ TO (CASE WHEN current_user = 'rls_regress_user1' THEN cid IN (11, 33)
+ WHEN current_user = 'rls_regress_user2' THEN cid IN (22, 44)
+ ELSE false END);
+-- cannot delete PK referenced by invisible FK
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document d full outer join category c on d.cid = c.cid;
+ did | cid | dlevel | dauthor | dtitle | cid | cname
+-----+-----+--------+-------------------+--------------------+-----+------------
+ 2 | 11 | 2 | rls_regress_user1 | my second novel | 11 | novel
+ 1 | 11 | 1 | rls_regress_user1 | my first novel | 11 | novel
+ | | | | | 33 | technology
+ 5 | 44 | 2 | rls_regress_user1 | my second manga | |
+ 4 | 44 | 1 | rls_regress_user1 | my first manga | |
+ 3 | 22 | 2 | rls_regress_user1 | my science fiction | |
+(6 rows)
+
+DELETE FROM category WHERE cid = 33; -- failed
+ERROR: update or delete on table "category" violates foreign key constraint "document_cid_fkey" on table "document"
+DETAIL: Key (cid)=(33) is still referenced from table "document".
+-- cannot insert FK referencing invisible PK
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document d full outer join category c on d.cid = c.cid;
+ did | cid | dlevel | dauthor | dtitle | cid | cname
+-----+-----+--------+-------------------+-----------------------+-----+-----------------
+ 6 | 22 | 1 | rls_regress_user2 | great science fiction | 22 | science fiction
+ 8 | 44 | 1 | rls_regress_user2 | great manga | 44 | manga
+ 7 | 33 | 2 | rls_regress_user2 | great technology book | |
+(3 rows)
+
+INSERT INTO document VALUES (10, 33, 1, current_user, 'hoge'); -- failed
+ERROR: insert or update on table "document" violates foreign key constraint "document_cid_fkey"
+DETAIL: Key (cid)=(33) is not present in table "category".
+-- database superuser can bypass RLS policy
+RESET SESSION AUTHORIZATION;
+SELECT * FROM document;
+ did | cid | dlevel | dauthor | dtitle
+-----+-----+--------+-------------------+-----------------------
+ 1 | 11 | 1 | rls_regress_user1 | my first novel
+ 2 | 11 | 2 | rls_regress_user1 | my second novel
+ 3 | 22 | 2 | rls_regress_user1 | my science fiction
+ 4 | 44 | 1 | rls_regress_user1 | my first manga
+ 5 | 44 | 2 | rls_regress_user1 | my second manga
+ 6 | 22 | 1 | rls_regress_user2 | great science fiction
+ 7 | 33 | 2 | rls_regress_user2 | great technology book
+ 8 | 44 | 1 | rls_regress_user2 | great manga
+(8 rows)
+
+SELECT * FROM category;
+ cid | cname
+-----+-----------------
+ 11 | novel
+ 22 | science fiction
+ 33 | technology
+ 44 | manga
+(4 rows)
+
+--
+-- Table inheritance and RLS policy
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+CREATE TABLE t1 (a int, junk1 text, b text) WITH OIDS;
+ALTER TABLE t1 DROP COLUMN junk1; -- just a disturbing factor
+GRANT ALL ON t1 TO public;
+COPY t1 FROM stdin WITH (oids);
+CREATE TABLE t2 (c float) INHERITS (t1);
+COPY t2 FROM stdin WITH (oids);
+CREATE TABLE t3 (c text, b text, a int) WITH OIDS;
+ALTER TABLE t3 INHERIT t1;
+COPY t3(a,b,c) FROM stdin WITH (oids);
+ALTER TABLE t1 SET ROW SECURITY FOR ALL TO (a % 2 = 0); -- be even number
+ALTER TABLE t2 SET ROW SECURITY FOR ALL TO (a % 2 = 1); -- be odd number
+SELECT * FROM t1;
+ a | b
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1;
+ QUERY PLAN
+-------------------------------------
+ Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+(8 rows)
+
+SELECT * FROM t1 WHERE f_leak(b);
+NOTICE: f_leak => bbb
+NOTICE: f_leak => ddd
+NOTICE: f_leak => abc
+NOTICE: f_leak => cde
+NOTICE: f_leak => xxx
+NOTICE: f_leak => yyy
+NOTICE: f_leak => zzz
+ a | b
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+ QUERY PLAN
+-------------------------------------
+ Append
+ -> Subquery Scan on t1
+ Filter: f_leak(t1.b)
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ Filter: f_leak(t2.b)
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+ Filter: f_leak(b)
+(11 rows)
+
+-- reference to system column
+SELECT oid, * FROM t1;
+ oid | a | b
+-----+---+-----
+ 102 | 2 | bbb
+ 104 | 4 | ddd
+ 201 | 1 | abc
+ 203 | 3 | cde
+ 301 | 1 | xxx
+ 302 | 2 | yyy
+ 303 | 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1;
+ QUERY PLAN
+-------------------------------------
+ Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+(8 rows)
+
+-- reference to whole-row reference
+SELECT *,t1 FROM t1;
+ a | b | t1
+---+-----+---------
+ 2 | bbb | (2,bbb)
+ 4 | ddd | (4,ddd)
+ 1 | abc | (1,abc)
+ 3 | cde | (3,cde)
+ 1 | xxx | (1,xxx)
+ 2 | yyy | (2,yyy)
+ 3 | zzz | (3,zzz)
+(7 rows)
+
+EXPLAIN (costs off) SELECT *,t1 FROM t1;
+ QUERY PLAN
+-------------------------------------
+ Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+(8 rows)
+
+-- for share/update lock
+SELECT * FROM t1 FOR SHARE;
+ a | b
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 FOR SHARE;
+ QUERY PLAN
+-------------------------------------------
+ LockRows
+ -> Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+(9 rows)
+
+SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+NOTICE: f_leak => bbb
+NOTICE: f_leak => ddd
+NOTICE: f_leak => abc
+NOTICE: f_leak => cde
+NOTICE: f_leak => xxx
+NOTICE: f_leak => yyy
+NOTICE: f_leak => zzz
+ a | b
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+ QUERY PLAN
+-------------------------------------------
+ LockRows
+ -> Append
+ -> Subquery Scan on t1
+ Filter: f_leak(t1.b)
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ Filter: f_leak(t2.b)
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+ Filter: f_leak(b)
+(12 rows)
+
+--
+-- COPY TO statement
+--
+COPY t1 TO stdout;
+2 bbb
+4 ddd
+COPY t1 TO stdout WITH OIDS;
+102 2 bbb
+104 4 ddd
+COPY t2(c,b) TO stdout WITH OIDS;
+201 1.1 abc
+203 3.3 cde
+COPY (SELECT * FROM t1) TO stdout;
+2 bbb
+4 ddd
+1 abc
+3 cde
+1 xxx
+2 yyy
+3 zzz
+COPY document TO stdout WITH OIDS; -- failed (no oid column)
+ERROR: table "document" does not have OIDs
+--
+-- recursive RLS and VIEWs in policy
+--
+CREATE TABLE s1 (a int, b text);
+INSERT INTO s1 (SELECT x, md5(x::text) FROM generate_series(-10,10) x);
+CREATE TABLE s2 (x int, y text);
+INSERT INTO s2 (SELECT x, md5(x::text) FROM generate_series(-6,6) x);
+CREATE VIEW v2 AS SELECT * FROM s2 WHERE y like '%af%';
+ALTER TABLE s1 SET ROW SECURITY FOR ALL
+ TO (a in (select x from s2 where y like '%2f%'));
+ALTER TABLE s2 SET ROW SECURITY FOR ALL
+ TO (x in (select a from s1 where b like '%22%'));
+SELECT * FROM s1 WHERE f_leak(b); -- fail (infinite recursion)
+ERROR: infinite recursion detected for relation "s1"
+ALTER TABLE s2 SET ROW SECURITY FOR ALL TO (x % 2 = 0);
+SELECT * FROM s1 WHERE f_leak(b); -- OK
+NOTICE: f_leak => c81e728d9d4c2f636f067f89cc14862c
+NOTICE: f_leak => a87ff679a2f3e71d9181a67b7542122c
+ a | b
+---+----------------------------------
+ 2 | c81e728d9d4c2f636f067f89cc14862c
+ 4 | a87ff679a2f3e71d9181a67b7542122c
+(2 rows)
+
+EXPLAIN SELECT * FROM only s1 WHERE f_leak(b);
+ QUERY PLAN
+---------------------------------------------------------------------------------------
+ Subquery Scan on s1 (cost=28.55..61.67 rows=205 width=36)
+ Filter: f_leak(s1.b)
+ -> Hash Join (cost=28.55..55.52 rows=615 width=36)
+ Hash Cond: (s1_1.a = s2.x)
+ -> Seq Scan on s1 s1_1 (cost=0.00..22.30 rows=1230 width=36)
+ -> Hash (cost=28.54..28.54 rows=1 width=4)
+ -> HashAggregate (cost=28.53..28.54 rows=1 width=4)
+ -> Subquery Scan on s2 (cost=0.00..28.52 rows=1 width=4)
+ Filter: (s2.y ~~ '%2f%'::text)
+ -> Seq Scan on s2 s2_1 (cost=0.00..28.45 rows=6 width=36)
+ Filter: ((x % 2) = 0)
+(11 rows)
+
+ALTER TABLE s1 SET ROW SECURITY FOR ALL
+ TO (a in (select x from v2)); -- using VIEW in RLS policy
+SELECT * FROM s1 WHERE f_leak(b); -- OK
+NOTICE: f_leak => 0267aaf632e87a63288a08331f22c7c3
+NOTICE: f_leak => 1679091c5a880faf6fb5e6087eb1b2dc
+ a | b
+----+----------------------------------
+ -4 | 0267aaf632e87a63288a08331f22c7c3
+ 6 | 1679091c5a880faf6fb5e6087eb1b2dc
+(2 rows)
+
+EXPLAIN (COSTS OFF) SELECT * FROM s1 WHERE f_leak(b);
+ QUERY PLAN
+----------------------------------------------------------
+ Subquery Scan on s1
+ Filter: f_leak(s1.b)
+ -> Hash Join
+ Hash Cond: (s1_1.a = s2.x)
+ -> Seq Scan on s1 s1_1
+ -> Hash
+ -> HashAggregate
+ -> Subquery Scan on s2
+ Filter: (s2.y ~~ '%af%'::text)
+ -> Seq Scan on s2 s2_1
+ Filter: ((x % 2) = 0)
+(11 rows)
+
+SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+ xx | x | y
+----+----+----------------------------------
+ -6 | -6 | 596a3d04481816330f07e4f97510c28f
+ -4 | -4 | 0267aaf632e87a63288a08331f22c7c3
+ 2 | 2 | c81e728d9d4c2f636f067f89cc14862c
+(3 rows)
+
+EXPLAIN (COSTS OFF) SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+ QUERY PLAN
+--------------------------------------------------------------------
+ Subquery Scan on s2
+ Filter: (s2.y ~~ '%28%'::text)
+ -> Seq Scan on s2 s2_1
+ Filter: ((x % 2) = 0)
+ SubPlan 1
+ -> Limit
+ -> Subquery Scan on s1
+ -> Nested Loop Semi Join
+ Join Filter: (s1_1.a = s2_2.x)
+ -> Seq Scan on s1 s1_1
+ -> Materialize
+ -> Subquery Scan on s2_2
+ Filter: (s2_2.y ~~ '%af%'::text)
+ -> Seq Scan on s2 s2_3
+ Filter: ((x % 2) = 0)
+(15 rows)
+
+ALTER TABLE s2 SET ROW SECURITY FOR ALL
+ TO (x in (select a from s1 where b like '%d2%'));
+SELECT * FROM s1 WHERE f_leak(b); -- fail (infinite recursion via view)
+ERROR: infinite recursion detected for relation "s1"
+-- prepared statement with rls_regress_user0 privilege
+PREPARE p1(int) AS SELECT * FROM t1 WHERE a <= $1;
+EXECUTE p1(2);
+ a | b
+---+-----
+ 2 | bbb
+ 1 | abc
+ 1 | xxx
+ 2 | yyy
+(4 rows)
+
+EXPLAIN (costs off) EXECUTE p1(2);
+ QUERY PLAN
+----------------------------------------------------
+ Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a <= 2) AND ((a % 2) = 0))
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a <= 2) AND ((a % 2) = 1))
+ -> Seq Scan on t3
+ Filter: (a <= 2)
+(9 rows)
+
+-- superuser is allowed to bypass RLS checks
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1 WHERE f_leak(b);
+NOTICE: f_leak => aaa
+NOTICE: f_leak => bbb
+NOTICE: f_leak => ccc
+NOTICE: f_leak => ddd
+NOTICE: f_leak => abc
+NOTICE: f_leak => bcd
+NOTICE: f_leak => cde
+NOTICE: f_leak => def
+NOTICE: f_leak => xxx
+NOTICE: f_leak => yyy
+NOTICE: f_leak => zzz
+ a | b
+---+-----
+ 1 | aaa
+ 2 | bbb
+ 3 | ccc
+ 4 | ddd
+ 1 | abc
+ 2 | bcd
+ 3 | cde
+ 4 | def
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(11 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+ QUERY PLAN
+---------------------------
+ Append
+ -> Seq Scan on t1
+ Filter: f_leak(b)
+ -> Seq Scan on t2
+ Filter: f_leak(b)
+ -> Seq Scan on t3
+ Filter: f_leak(b)
+(7 rows)
+
+-- plan cache should be invalidated
+EXECUTE p1(2);
+ a | b
+---+-----
+ 1 | aaa
+ 2 | bbb
+ 1 | abc
+ 2 | bcd
+ 1 | xxx
+ 2 | yyy
+(6 rows)
+
+EXPLAIN (costs off) EXECUTE p1(2);
+ QUERY PLAN
+--------------------------
+ Append
+ -> Seq Scan on t1
+ Filter: (a <= 2)
+ -> Seq Scan on t2
+ Filter: (a <= 2)
+ -> Seq Scan on t3
+ Filter: (a <= 2)
+(7 rows)
+
+PREPARE p2(int) AS SELECT * FROM t1 WHERE a = $1;
+EXECUTE p2(2);
+ a | b
+---+-----
+ 2 | bbb
+ 2 | bcd
+ 2 | yyy
+(3 rows)
+
+EXPLAIN (costs off) EXECUTE p2(2);
+ QUERY PLAN
+-------------------------
+ Append
+ -> Seq Scan on t1
+ Filter: (a = 2)
+ -> Seq Scan on t2
+ Filter: (a = 2)
+ -> Seq Scan on t3
+ Filter: (a = 2)
+(7 rows)
+
+-- also, case when privilege switch from superuser
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXECUTE p2(2);
+ a | b
+---+-----
+ 2 | bbb
+ 2 | yyy
+(2 rows)
+
+EXPLAIN (costs off) EXECUTE p2(2);
+ QUERY PLAN
+---------------------------------------------------
+ Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a = 2) AND ((a % 2) = 0))
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a = 2) AND ((a % 2) = 1))
+ -> Seq Scan on t3
+ Filter: (a = 2)
+(9 rows)
+
+--
+-- UPDATE / DELETE and Row-level security
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXPLAIN (costs off) UPDATE t1 SET b = b || b WHERE f_leak(b);
+ QUERY PLAN
+-------------------------------------
+ Update on t1
+ -> Subquery Scan on t1_1
+ Filter: f_leak(t1_1.b)
+ -> Seq Scan on t1 t1_2
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ Filter: f_leak(t2.b)
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+ Filter: f_leak(b)
+(11 rows)
+
+UPDATE t1 SET b = b || b WHERE f_leak(b);
+NOTICE: f_leak => bbb
+NOTICE: f_leak => ddd
+NOTICE: f_leak => abc
+NOTICE: f_leak => cde
+NOTICE: f_leak => xxx
+NOTICE: f_leak => yyy
+NOTICE: f_leak => zzz
+EXPLAIN (costs off) UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b);
+ QUERY PLAN
+-------------------------------------
+ Update on t1
+ -> Subquery Scan on t1_1
+ Filter: f_leak(t1_1.b)
+ -> Seq Scan on t1 t1_2
+ Filter: ((a % 2) = 0)
+(5 rows)
+
+UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b);
+NOTICE: f_leak => bbbbbb
+NOTICE: f_leak => dddddd
+-- returning clause with system column
+UPDATE only t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+NOTICE: f_leak => bbbbbb_updt
+NOTICE: f_leak => dddddd_updt
+ oid | a | b | t1
+-----+---+-------------+-----------------
+ 102 | 2 | bbbbbb_updt | (2,bbbbbb_updt)
+ 104 | 4 | dddddd_updt | (4,dddddd_updt)
+(2 rows)
+
+UPDATE t1 SET b = b WHERE f_leak(b) RETURNING *;
+NOTICE: f_leak => bbbbbb_updt
+NOTICE: f_leak => dddddd_updt
+NOTICE: f_leak => abcabc
+NOTICE: f_leak => cdecde
+NOTICE: f_leak => xxxxxx
+NOTICE: f_leak => yyyyyy
+NOTICE: f_leak => zzzzzz
+ a | b
+---+-------------
+ 2 | bbbbbb_updt
+ 4 | dddddd_updt
+ 1 | abcabc
+ 3 | cdecde
+ 1 | xxxxxx
+ 2 | yyyyyy
+ 3 | zzzzzz
+(7 rows)
+
+UPDATE t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+NOTICE: f_leak => bbbbbb_updt
+NOTICE: f_leak => dddddd_updt
+NOTICE: f_leak => abcabc
+NOTICE: f_leak => cdecde
+NOTICE: f_leak => xxxxxx
+NOTICE: f_leak => yyyyyy
+NOTICE: f_leak => zzzzzz
+ oid | a | b | t1
+-----+---+-------------+-----------------
+ 102 | 2 | bbbbbb_updt | (2,bbbbbb_updt)
+ 104 | 4 | dddddd_updt | (4,dddddd_updt)
+ 201 | 1 | abcabc | (1,abcabc)
+ 203 | 3 | cdecde | (3,cdecde)
+ 301 | 1 | xxxxxx | (1,xxxxxx)
+ 302 | 2 | yyyyyy | (2,yyyyyy)
+ 303 | 3 | zzzzzz | (3,zzzzzz)
+(7 rows)
+
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1;
+ a | b
+---+-------------
+ 1 | aaa
+ 3 | ccc
+ 2 | bbbbbb_updt
+ 4 | dddddd_updt
+ 2 | bcd
+ 4 | def
+ 1 | abcabc
+ 3 | cdecde
+ 1 | xxxxxx
+ 2 | yyyyyy
+ 3 | zzzzzz
+(11 rows)
+
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXPLAIN (costs off) DELETE FROM only t1 WHERE f_leak(b);
+ QUERY PLAN
+-------------------------------------
+ Delete on t1
+ -> Subquery Scan on t1_1
+ Filter: f_leak(t1_1.b)
+ -> Seq Scan on t1 t1_2
+ Filter: ((a % 2) = 0)
+(5 rows)
+
+EXPLAIN (costs off) DELETE FROM t1 WHERE f_leak(b);
+ QUERY PLAN
+-------------------------------------
+ Delete on t1
+ -> Subquery Scan on t1_1
+ Filter: f_leak(t1_1.b)
+ -> Seq Scan on t1 t1_2
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ Filter: f_leak(t2.b)
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+ Filter: f_leak(b)
+(11 rows)
+
+DELETE FROM only t1 WHERE f_leak(b) RETURNING oid, *, t1;
+NOTICE: f_leak => bbbbbb_updt
+NOTICE: f_leak => dddddd_updt
+ oid | a | b | t1
+-----+---+-------------+-----------------
+ 102 | 2 | bbbbbb_updt | (2,bbbbbb_updt)
+ 104 | 4 | dddddd_updt | (4,dddddd_updt)
+(2 rows)
+
+DELETE FROM t1 WHERE f_leak(b) RETURNING oid, *, t1;
+NOTICE: f_leak => abcabc
+NOTICE: f_leak => cdecde
+NOTICE: f_leak => xxxxxx
+NOTICE: f_leak => yyyyyy
+NOTICE: f_leak => zzzzzz
+ oid | a | b | t1
+-----+---+--------+------------
+ 201 | 1 | abcabc | (1,abcabc)
+ 203 | 3 | cdecde | (3,cdecde)
+ 301 | 1 | xxxxxx | (1,xxxxxx)
+ 302 | 2 | yyyyyy | (2,yyyyyy)
+ 303 | 3 | zzzzzz | (3,zzzzzz)
+(5 rows)
+
+--
+-- Test psql \dt+ command
+--
+ALTER TABLE category RESET ROW SECURITY FOR ALL; -- too long qual
+\dt+
+ List of relations
+ Schema | Name | Type | Owner | Size | Description | Row-security
+--------------------+----------+-------+-------------------+------------+-------------+----------------------------------
+ rls_regress_schema | category | table | rls_regress_user0 | 16 kB | |
+ rls_regress_schema | document | table | rls_regress_user0 | 16 kB | | (dauthor = "current_user"())
+ rls_regress_schema | s1 | table | rls_regress_user0 | 16 kB | | (a IN ( SELECT v2.x +
+ | | | | | | FROM v2))
+ rls_regress_schema | s2 | table | rls_regress_user0 | 16 kB | | (x IN ( SELECT s1.a +
+ | | | | | | FROM s1 +
+ | | | | | | WHERE (s1.b ~~ '%d2%'::text)))
+ rls_regress_schema | t1 | table | rls_regress_user0 | 16 kB | | ((a % 2) = 0)
+ rls_regress_schema | t2 | table | rls_regress_user0 | 16 kB | | ((a % 2) = 1)
+ rls_regress_schema | t3 | table | rls_regress_user0 | 16 kB | |
+ rls_regress_schema | uaccount | table | rls_regress_user0 | 8192 bytes | |
+(8 rows)
+
+--
+-- Clean up objects
+--
+RESET SESSION AUTHORIZATION;
+DROP SCHEMA rls_regress_schema CASCADE;
+NOTICE: drop cascades to 10 other objects
+DETAIL: drop cascades to function f_leak(text)
+drop cascades to table uaccount
+drop cascades to table category
+drop cascades to table document
+drop cascades to table t1
+drop cascades to table t2
+drop cascades to table t3
+drop cascades to table s1
+drop cascades to table s2
+drop cascades to view v2
+DROP USER rls_regress_user0;
+DROP USER rls_regress_user1;
+DROP USER rls_regress_user2;
diff --git a/src/test/regress/expected/sanity_check.out b/src/test/regress/expected/sanity_check.out
index 432d39a..3b9123d 100644
--- a/src/test/regress/expected/sanity_check.out
+++ b/src/test/regress/expected/sanity_check.out
@@ -120,6 +120,7 @@ SELECT relname, relhasindex
pg_proc | t
pg_range | t
pg_rewrite | t
+ pg_rowsecurity | t
pg_seclabel | t
pg_shdepend | t
pg_shdescription | t
@@ -166,7 +167,7 @@ SELECT relname, relhasindex
timetz_tbl | f
tinterval_tbl | f
varchar_tbl | f
-(155 rows)
+(156 rows)
--
-- another sanity check: every system catalog that has OIDs should have
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index fd08e8d..367df00 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 matview lock
+test: privileges rowsecurity security_label collate matview lock
# ----------
# Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 1ed059b..5883a64 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -93,6 +93,7 @@ test: delete
test: namespace
test: prepared_xacts
test: privileges
+test: rowsecurity
test: security_label
test: collate
test: matview
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
new file mode 100644
index 0000000..55d4aad
--- /dev/null
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -0,0 +1,298 @@
+--
+-- Test of Row-level security feature
+--
+
+-- Clean up in case a prior regression run failed
+
+-- Suppress NOTICE messages when users/groups don't exist
+SET client_min_messages TO 'warning';
+
+DROP USER IF EXISTS rls_regress_user0;
+DROP USER IF EXISTS rls_regress_user1;
+DROP USER IF EXISTS rls_regress_user2;
+
+DROP SCHEMA IF EXISTS rls_regress_schema CASCADE;
+
+RESET client_min_messages;
+
+-- initial setup
+CREATE USER rls_regress_user0;
+CREATE USER rls_regress_user1;
+CREATE USER rls_regress_user2;
+
+CREATE SCHEMA rls_regress_schema;
+GRANT ALL ON SCHEMA rls_regress_schema TO public;
+SET search_path = rls_regress_schema;
+
+-- setup of malicious function
+CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool
+ COST 0.0000001 LANGUAGE plpgsql
+ AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END';
+GRANT EXECUTE ON FUNCTION f_leak(text) TO public;
+
+-- BASIC Row-Level Security Scenario
+
+SET SESSION AUTHORIZATION rls_regress_user0;
+CREATE TABLE uaccount (
+ pguser name primary key,
+ seclv int
+);
+INSERT INTO uaccount VALUES
+ ('rls_regress_user0', 99),
+ ('rls_regress_user1', 1),
+ ('rls_regress_user2', 2),
+ ('rls_regress_user3', 3);
+GRANT SELECT ON uaccount TO public;
+
+CREATE TABLE category (
+ cid int primary key,
+ cname text
+);
+GRANT ALL ON category TO public;
+INSERT INTO category VALUES
+ (11, 'novel'),
+ (22, 'science fiction'),
+ (33, 'technology'),
+ (44, 'manga');
+
+CREATE TABLE document (
+ did int primary key,
+ cid int references category(cid),
+ dlevel int not null,
+ dauthor name,
+ dtitle text
+);
+GRANT ALL ON document TO public;
+INSERT INTO document VALUES
+ ( 1, 11, 1, 'rls_regress_user1', 'my first novel'),
+ ( 2, 11, 2, 'rls_regress_user1', 'my second novel'),
+ ( 3, 22, 2, 'rls_regress_user1', 'my science fiction'),
+ ( 4, 44, 1, 'rls_regress_user1', 'my first manga'),
+ ( 5, 44, 2, 'rls_regress_user1', 'my second manga'),
+ ( 6, 22, 1, 'rls_regress_user2', 'great science fiction'),
+ ( 7, 33, 2, 'rls_regress_user2', 'great technology book'),
+ ( 8, 44, 1, 'rls_regress_user2', 'great manga');
+
+-- user's security level must higher than or equal to document's one
+ALTER TABLE document SET ROW SECURITY FOR ALL
+ TO (dlevel <= (SELECT seclv FROM uaccount WHERE pguser = current_user));
+
+-- viewpoint from rls_regress_user1
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+-- viewpoint from rls_regress_user2
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+-- only owner can change row-level security
+ALTER TABLE document SET ROW SECURITY FOR ALL TO (true); -- fail
+ALTER TABLE document RESET ROW SECURITY FOR ALL; -- fail
+
+SET SESSION AUTHORIZATION rls_regress_user0;
+ALTER TABLE document SET ROW SECURITY FOR ALL TO (dauthor = current_user);
+
+-- viewpoint from rls_regress_user1 again
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+-- viewpoint from rls_regress_user2 again
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+-- interaction of FK/PK constraints
+SET SESSION AUTHORIZATION rls_regress_user0;
+ALTER TABLE category SET ROW SECURITY FOR ALL
+ TO (CASE WHEN current_user = 'rls_regress_user1' THEN cid IN (11, 33)
+ WHEN current_user = 'rls_regress_user2' THEN cid IN (22, 44)
+ ELSE false END);
+
+-- cannot delete PK referenced by invisible FK
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document d full outer join category c on d.cid = c.cid;
+DELETE FROM category WHERE cid = 33; -- failed
+
+-- cannot insert FK referencing invisible PK
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document d full outer join category c on d.cid = c.cid;
+INSERT INTO document VALUES (10, 33, 1, current_user, 'hoge'); -- failed
+
+-- database superuser can bypass RLS policy
+RESET SESSION AUTHORIZATION;
+SELECT * FROM document;
+SELECT * FROM category;
+
+--
+-- Table inheritance and RLS policy
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+
+CREATE TABLE t1 (a int, junk1 text, b text) WITH OIDS;
+ALTER TABLE t1 DROP COLUMN junk1; -- just a disturbing factor
+GRANT ALL ON t1 TO public;
+
+COPY t1 FROM stdin WITH (oids);
+101 1 aaa
+102 2 bbb
+103 3 ccc
+104 4 ddd
+\.
+
+CREATE TABLE t2 (c float) INHERITS (t1);
+COPY t2 FROM stdin WITH (oids);
+201 1 abc 1.1
+202 2 bcd 2.2
+203 3 cde 3.3
+204 4 def 4.4
+\.
+
+CREATE TABLE t3 (c text, b text, a int) WITH OIDS;
+ALTER TABLE t3 INHERIT t1;
+COPY t3(a,b,c) FROM stdin WITH (oids);
+301 1 xxx X
+302 2 yyy Y
+303 3 zzz Z
+\.
+
+ALTER TABLE t1 SET ROW SECURITY FOR ALL TO (a % 2 = 0); -- be even number
+ALTER TABLE t2 SET ROW SECURITY FOR ALL TO (a % 2 = 1); -- be odd number
+
+SELECT * FROM t1;
+EXPLAIN (costs off) SELECT * FROM t1;
+
+SELECT * FROM t1 WHERE f_leak(b);
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+
+-- reference to system column
+SELECT oid, * FROM t1;
+EXPLAIN (costs off) SELECT * FROM t1;
+
+-- reference to whole-row reference
+SELECT *,t1 FROM t1;
+EXPLAIN (costs off) SELECT *,t1 FROM t1;
+
+-- for share/update lock
+SELECT * FROM t1 FOR SHARE;
+EXPLAIN (costs off) SELECT * FROM t1 FOR SHARE;
+
+SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+
+--
+-- COPY TO statement
+--
+COPY t1 TO stdout;
+COPY t1 TO stdout WITH OIDS;
+COPY t2(c,b) TO stdout WITH OIDS;
+COPY (SELECT * FROM t1) TO stdout;
+COPY document TO stdout WITH OIDS; -- failed (no oid column)
+
+--
+-- recursive RLS and VIEWs in policy
+--
+CREATE TABLE s1 (a int, b text);
+INSERT INTO s1 (SELECT x, md5(x::text) FROM generate_series(-10,10) x);
+
+CREATE TABLE s2 (x int, y text);
+INSERT INTO s2 (SELECT x, md5(x::text) FROM generate_series(-6,6) x);
+CREATE VIEW v2 AS SELECT * FROM s2 WHERE y like '%af%';
+
+ALTER TABLE s1 SET ROW SECURITY FOR ALL
+ TO (a in (select x from s2 where y like '%2f%'));
+
+ALTER TABLE s2 SET ROW SECURITY FOR ALL
+ TO (x in (select a from s1 where b like '%22%'));
+
+SELECT * FROM s1 WHERE f_leak(b); -- fail (infinite recursion)
+
+ALTER TABLE s2 SET ROW SECURITY FOR ALL TO (x % 2 = 0);
+
+SELECT * FROM s1 WHERE f_leak(b); -- OK
+EXPLAIN SELECT * FROM only s1 WHERE f_leak(b);
+
+ALTER TABLE s1 SET ROW SECURITY FOR ALL
+ TO (a in (select x from v2)); -- using VIEW in RLS policy
+SELECT * FROM s1 WHERE f_leak(b); -- OK
+EXPLAIN (COSTS OFF) SELECT * FROM s1 WHERE f_leak(b);
+
+SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+EXPLAIN (COSTS OFF) SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+
+ALTER TABLE s2 SET ROW SECURITY FOR ALL
+ TO (x in (select a from s1 where b like '%d2%'));
+SELECT * FROM s1 WHERE f_leak(b); -- fail (infinite recursion via view)
+
+-- prepared statement with rls_regress_user0 privilege
+PREPARE p1(int) AS SELECT * FROM t1 WHERE a <= $1;
+EXECUTE p1(2);
+EXPLAIN (costs off) EXECUTE p1(2);
+
+-- superuser is allowed to bypass RLS checks
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1 WHERE f_leak(b);
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+
+-- plan cache should be invalidated
+EXECUTE p1(2);
+EXPLAIN (costs off) EXECUTE p1(2);
+
+PREPARE p2(int) AS SELECT * FROM t1 WHERE a = $1;
+EXECUTE p2(2);
+EXPLAIN (costs off) EXECUTE p2(2);
+
+-- also, case when privilege switch from superuser
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXECUTE p2(2);
+EXPLAIN (costs off) EXECUTE p2(2);
+
+--
+-- UPDATE / DELETE and Row-level security
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXPLAIN (costs off) UPDATE t1 SET b = b || b WHERE f_leak(b);
+UPDATE t1 SET b = b || b WHERE f_leak(b);
+
+EXPLAIN (costs off) UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b);
+UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b);
+
+-- returning clause with system column
+UPDATE only t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+UPDATE t1 SET b = b WHERE f_leak(b) RETURNING *;
+UPDATE t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1;
+
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXPLAIN (costs off) DELETE FROM only t1 WHERE f_leak(b);
+EXPLAIN (costs off) DELETE FROM t1 WHERE f_leak(b);
+
+DELETE FROM only t1 WHERE f_leak(b) RETURNING oid, *, t1;
+DELETE FROM t1 WHERE f_leak(b) RETURNING oid, *, t1;
+
+--
+-- Test psql \dt+ command
+--
+ALTER TABLE category RESET ROW SECURITY FOR ALL; -- too long qual
+\dt+
+
+--
+-- Clean up objects
+--
+RESET SESSION AUTHORIZATION;
+
+DROP SCHEMA rls_regress_schema CASCADE;
+
+DROP USER rls_regress_user0;
+DROP USER rls_regress_user1;
+DROP USER rls_regress_user2;
2013/8/28 Oleg Bartunov <obartunov@gmail.com>:
btw, there is serious problem with row-level security and constraints. For
example, user with low security level could use unique constraint to know
about existence of a row with higher security. I don't know, what is the
best practice to avoid this.
It is out of scope for this feature. We usually calls this type of information
leakage "covert channel"; that is not avoidable in principle.
However, its significance is minor, because attacker must know identical
data to be here, or must have proving for each possible values.
Its solution is simple. DBA should not use value to be confidential as unique
key. If needed, our recommendation is alternative key, instead of natural key,
because its value itself does not have worth.
I should add a note of caution onto the documentation according to
the previous consensus, however, I noticed it had gone from the sgml files
while I was unaware. So, let me add description on the documentation.
Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Any constraints could be "covert channel".
On Wed, Aug 28, 2013 at 4:17 PM, Kohei KaiGai <kaigai@kaigai.gr.jp> wrote:
Show quoted text
2013/8/28 Oleg Bartunov <obartunov@gmail.com>:
btw, there is serious problem with row-level security and constraints.
For
example, user with low security level could use unique constraint to know
about existence of a row with higher security. I don't know, what is the
best practice to avoid this.It is out of scope for this feature. We usually calls this type of
information
leakage "covert channel"; that is not avoidable in principle.
However, its significance is minor, because attacker must know identical
data to be here, or must have proving for each possible values.
Its solution is simple. DBA should not use value to be confidential as
unique
key. If needed, our recommendation is alternative key, instead of natural
key,
because its value itself does not have worth.I should add a note of caution onto the documentation according to
the previous consensus, however, I noticed it had gone from the sgml files
while I was unaware. So, let me add description on the documentation.Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
On Wed, Aug 28, 2013 at 4:17 PM, Kohei KaiGai <kaigai@kaigai.gr.jp> wrote:
2013/8/28 Oleg Bartunov <obartunov@gmail.com>:
btw, there is serious problem with row-level security and constraints.
For
example, user with low security level could use unique constraint to know
about existence of a row with higher security. I don't know, what is the
best practice to avoid this.It is out of scope for this feature. We usually calls this type of
information
leakage "covert channel"; that is not avoidable in principle.
However, its significance is minor, because attacker must know identical
data to be here, or must have proving for each possible values.
Its solution is simple. DBA should not use value to be confidential as
unique
key. If needed, our recommendation is alternative key, instead of natural
key,
because its value itself does not have worth.I should add a note of caution onto the documentation according to
the previous consensus, however, I noticed it had gone from the sgml files
while I was unaware. So, let me add description on the documentation.
I think there is another "covert channel" much more serious than
constrains. You can gather information about hidden data by reading query
plans.
CREATE TABLE test (id SERIAL PRIMARY KEY, value INTEGER);
INSERT INTO test (value) VALUES (1234), (4321), (2356), (6542);
ALTER TABLE test SET ROW SECURITY FOR ALL TO (id % 2 = 1);
CREATE INDEX test_idx ON test (value);
User sees only 1 and 3 rows:
postgres=> select * from test;
id | value
----+-------
1 | 1234
3 | 2356
(2 rows)
But user can probe values column using explain command.
postgres=> set enable_seqscan = off;
SET
postgres=> explain analyze select * from test where value between 1111 and
3333;
QUERY PLAN
---------------------------------------------------------------------------------------------------------------
Index Scan using test_idx on test (cost=0.13..8.16 rows=1 width=8)
(actual time=0.021..0.024 rows=2 loops=1)
Index Cond: ((value >= 1111) AND (value <= 3333))
Filter: ((id % 2) = 1)
Total runtime: 0.056 ms
(4 rows)
postgres=> explain analyze select * from test where value between 1111 and
5555;
QUERY PLAN
---------------------------------------------------------------------------------------------------------------
Index Scan using test_idx on test (cost=0.13..8.16 rows=1 width=8)
(actual time=0.020..0.024 rows=2 loops=1)
Index Cond: ((value >= 1111) AND (value <= 5555))
Filter: ((id % 2) = 1)
Rows Removed by Filter: 1
Total runtime: 0.057 ms
(5 rows)
In given example user can realize that there is a hidden value in index
between 3334 and 5555. Using dichotomy he can find exact value.
I didn't find if there was discussion about it. This example is only my
first idea about using plans for probing hidden values. Probably, there are
some other ways to do it.
I don't think we can say that indexed data is not sensitive for leakage.
Prohibiting push down of all operators which could be used for such kind of
attacks also doesn't seem acceptable for me because of huge impact to
performance. Another option I see is to hide some sensitive parts of plan
from unprivileged user. There is still a room for timing attack, but it
doesn't seem to be feasible in practice to apply.
------
With best regards,
Alexander Korotkov.
Alexander Korotkov <aekorotkov@gmail.com> writes:
On Wed, Aug 28, 2013 at 4:17 PM, Kohei KaiGai <kaigai@kaigai.gr.jp> wrote:
It is out of scope for this feature. We usually calls this type of
information leakage "covert channel"; that is not avoidable in principle.
I think there is another "covert channel" much more serious than
constrains. You can gather information about hidden data by reading query
plans.
I'm not convinced by this argument that covert channels are "out of
scope". That would be a fine justification for, say, a thesis topic.
However, what we're talking about here is a real-world feature that will
be of no real-world use if it can't stand up against rather obvious attack
techniques. I'm not interested in carrying the maintenance and runtime
overhead of a feature that's only of academic value.
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
2013/8/29 Alexander Korotkov <aekorotkov@gmail.com>:
On Wed, Aug 28, 2013 at 4:17 PM, Kohei KaiGai <kaigai@kaigai.gr.jp> wrote:
2013/8/28 Oleg Bartunov <obartunov@gmail.com>:
btw, there is serious problem with row-level security and constraints.
For
example, user with low security level could use unique constraint to
know
about existence of a row with higher security. I don't know, what is
the
best practice to avoid this.It is out of scope for this feature. We usually calls this type of
information
leakage "covert channel"; that is not avoidable in principle.
However, its significance is minor, because attacker must know identical
data to be here, or must have proving for each possible values.
Its solution is simple. DBA should not use value to be confidential as
unique
key. If needed, our recommendation is alternative key, instead of natural
key,
because its value itself does not have worth.I should add a note of caution onto the documentation according to
the previous consensus, however, I noticed it had gone from the sgml files
while I was unaware. So, let me add description on the documentation.I think there is another "covert channel" much more serious than constrains.
You can gather information about hidden data by reading query plans.CREATE TABLE test (id SERIAL PRIMARY KEY, value INTEGER);
INSERT INTO test (value) VALUES (1234), (4321), (2356), (6542);
ALTER TABLE test SET ROW SECURITY FOR ALL TO (id % 2 = 1);
CREATE INDEX test_idx ON test (value);User sees only 1 and 3 rows:
postgres=> select * from test;
id | value
----+-------
1 | 1234
3 | 2356
(2 rows)But user can probe values column using explain command.
postgres=> set enable_seqscan = off;
SET
postgres=> explain analyze select * from test where value between 1111 and
3333;
QUERY PLAN
---------------------------------------------------------------------------------------------------------------
Index Scan using test_idx on test (cost=0.13..8.16 rows=1 width=8) (actual
time=0.021..0.024 rows=2 loops=1)
Index Cond: ((value >= 1111) AND (value <= 3333))
Filter: ((id % 2) = 1)
Total runtime: 0.056 ms
(4 rows)postgres=> explain analyze select * from test where value between 1111 and
5555;
QUERY PLAN
---------------------------------------------------------------------------------------------------------------
Index Scan using test_idx on test (cost=0.13..8.16 rows=1 width=8) (actual
time=0.020..0.024 rows=2 loops=1)
Index Cond: ((value >= 1111) AND (value <= 5555))
Filter: ((id % 2) = 1)
Rows Removed by Filter: 1
Total runtime: 0.057 ms
(5 rows)In given example user can realize that there is a hidden value in index
between 3334 and 5555. Using dichotomy he can find exact value.
I didn't find if there was discussion about it. This example is only my
first idea about using plans for probing hidden values. Probably, there are
some other ways to do it.
Hmm. It is a new scenario for me, even though its basis is similar to
the scenario that abuses constraints because it still does not leak
the hidden value itself and it requires "estimation" by human.
If we tackle this issue, an easy solution is to hide "rows removed by
filter" output if plan is underlying sub-query plan that was constructed
by row-level security feature.
I don't think we can say that indexed data is not sensitive for leakage.
Prohibiting push down of all operators which could be used for such kind of
attacks also doesn't seem acceptable for me because of huge impact to
performance. Another option I see is to hide some sensitive parts of plan
from unprivileged user. There is still a room for timing attack, but it
doesn't seem to be feasible in practice to apply.
A principle of this row-level security feature is, it prohibits to
leak invisible
datum itself, but might allow users to expect existence of records with
a particular value. In fact, we never push down function that may leak
the given argument, that does not have leakproof attribute, even if it can
be utilized for index-scan.
My opinion is, we should deal with it is "a limitation" of this feature, as
long as it does not expose the raw data to be hidden. Estimation takes
time to carry out much hidden data via covert channel, thus traditional
secure operating system specification with MAC implementation says
its degree of threat is not significant as long as bandwidth of covert
channel is not so much. I think it is a reasonable standpoint.
Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Thu, Aug 29, 2013 at 04:14:53PM +0200, Kohei KaiGai wrote:
2013/8/29 Alexander Korotkov <aekorotkov@gmail.com>:
On Wed, Aug 28, 2013 at 4:17 PM, Kohei KaiGai <kaigai@kaigai.gr.jp> wrote:
2013/8/28 Oleg Bartunov <obartunov@gmail.com>:
btw, there is serious problem with row-level security and constraints.
For
example, user with low security level could use unique constraint to
know
about existence of a row with higher security. I don't know, what is
the
best practice to avoid this.
...
A principle of this row-level security feature is, it prohibits to
leak invisible
datum itself, but might allow users to expect existence of records with
a particular value. In fact, we never push down function that may leak
the given argument, that does not have leakproof attribute, even if it can
be utilized for index-scan.
My opinion is, we should deal with it is "a limitation" of this feature, as
long as it does not expose the raw data to be hidden. Estimation takes
time to carry out much hidden data via covert channel, thus traditional
secure operating system specification with MAC implementation says
its degree of threat is not significant as long as bandwidth of covert
channel is not so much. I think it is a reasonable standpoint.Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
Okay, given that argument, how would you monitor such attempts to access
data through the covert channel and shut it down?
Regards,
Ken
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
2013/8/29 ktm@rice.edu <ktm@rice.edu>:
On Thu, Aug 29, 2013 at 04:14:53PM +0200, Kohei KaiGai wrote:
2013/8/29 Alexander Korotkov <aekorotkov@gmail.com>:
On Wed, Aug 28, 2013 at 4:17 PM, Kohei KaiGai <kaigai@kaigai.gr.jp> wrote:
2013/8/28 Oleg Bartunov <obartunov@gmail.com>:
btw, there is serious problem with row-level security and constraints.
For
example, user with low security level could use unique constraint to
know
about existence of a row with higher security. I don't know, what is
the
best practice to avoid this....
A principle of this row-level security feature is, it prohibits to
leak invisible
datum itself, but might allow users to expect existence of records with
a particular value. In fact, we never push down function that may leak
the given argument, that does not have leakproof attribute, even if it can
be utilized for index-scan.
My opinion is, we should deal with it is "a limitation" of this feature, as
long as it does not expose the raw data to be hidden. Estimation takes
time to carry out much hidden data via covert channel, thus traditional
secure operating system specification with MAC implementation says
its degree of threat is not significant as long as bandwidth of covert
channel is not so much. I think it is a reasonable standpoint.Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>Okay, given that argument, how would you monitor such attempts to access
data through the covert channel and shut it down?
Although I didn't touch this task by myself, my senior colleague said that we
calculated some possible bandwidth of leakage as an evident when we ship
supercomputer system with MAC feature for customer who worry about security.
I'm not sure how to measure it. However, if we assume a human can run up to
5 query per seconds, he needs 2-3 seconds to identify a particular integer value
less than 10000, it means bandwidth of this covert channel is less than 5bps.
I'm not sure whether enterprise-grade dbms has to care about these amount
of covert channel.
Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Kaigai,
Although I didn't touch this task by myself, my senior colleague said that we
calculated some possible bandwidth of leakage as an evident when we ship
supercomputer system with MAC feature for customer who worry about security.
I'm not sure how to measure it. However, if we assume a human can run up to
5 query per seconds, he needs 2-3 seconds to identify a particular integer value
less than 10000, it means bandwidth of this covert channel is less than 5bps.
I'm not sure whether enterprise-grade dbms has to care about these amount
of covert channel.
Why are you assuming a human needs to do it? Given the explain vector,
I could write a rather simple python or perl script which would find
values by EXPLAIN leakage, at 1000 explain plans per minute.
It's one thing to day "we can't solve this covert channel issue right
now in this patch", but saying "we don't plan to solve it at all" is
likely to doom the patch.
I'm not sure what the solution would be, exactly. Deny permission for
EXPLAIN on certain tables?
Surely someone in the security community has discussed this?
--
Josh Berkus
PostgreSQL Experts Inc.
http://pgexperts.com
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Import Notes
Reply to msg id not found: WM685df689dd4d8f8c5349d5ef7b8e285db1e99ea32d22ab5d35f3981c29eac19dbd725c9b5e0227def9d782de274d1354@asav-3.01.com
* Kohei KaiGai (kaigai@kaigai.gr.jp) wrote:
Although I didn't touch this task by myself, my senior colleague said that we
calculated some possible bandwidth of leakage as an evident when we ship
supercomputer system with MAC feature for customer who worry about security.
I'm not sure how to measure it. However, if we assume a human can run up to
5 query per seconds, he needs 2-3 seconds to identify a particular integer value
less than 10000, it means bandwidth of this covert channel is less than 5bps.
I'm not sure whether enterprise-grade dbms has to care about these amount
of covert channel.
A human isn't necessary in this particular scenario- you're doing a
simple binary search through the space, which computers can be awful
good at. Using the type's bounds (eg: an 'integer' field is only
32bits) and forcing index scans, you could tell how many records exist
and then break down how many exist in the first half, second half, and
then split those.. Eventually, you could work out every value in the
column.
That could be applied to variable length values also, though it'd be
more costly to get down to the exact values.
I don't have a solution to these issues offhand except to suggest that,
in such an environment, having a "don't allow these users to run
EXPLAIN" and similar options would probably be welcome. Even then there
are potential timing attacks, but that certainly increases the level of
effort involved.
Thanks,
Stephen
Josh Berkus <josh@agliodbs.com> writes:
It's one thing to day "we can't solve this covert channel issue right
now in this patch", but saying "we don't plan to solve it at all" is
likely to doom the patch.
I'm not sure what the solution would be, exactly. Deny permission for
EXPLAIN on certain tables?
That would close only one covert channel. Others were already pointed out
upthread, and I'll bet there are more ...
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
I'm not sure what the solution would be, exactly. Deny permission for
EXPLAIN on certain tables?That would close only one covert channel. Others were already pointed out
upthread, and I'll bet there are more ...
Mind you, fundamentally this is no different from allowing INSERT
permission on a table but denying SELECT, or denying SELECT on certain
columns. In either case, covert channels for some data are available.
--
Josh Berkus
PostgreSQL Experts Inc.
http://pgexperts.com
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Import Notes
Reply to msg id not found: WMb776adf5b5f415ff9a8c49026fe72462f8b32852bb3327d6185064e91ae314e93bda384c75db979881f53ae4e36664a7@asav-1.01.com
On Thu, Aug 29, 2013 at 10:05:14AM -0400, Tom Lane wrote:
Alexander Korotkov <aekorotkov@gmail.com> writes:
On Wed, Aug 28, 2013 at 4:17 PM, Kohei KaiGai <kaigai@kaigai.gr.jp> wrote:
It is out of scope for this feature. We usually calls this type
of information leakage "covert channel"; that is not avoidable in
principle.I think there is another "covert channel" much more serious than
constrains. You can gather information about hidden data by
reading query plans.I'm not convinced by this argument that covert channels are "out of
scope". That would be a fine justification for, say, a thesis
topic. However, what we're talking about here is a real-world
feature that will be of no real-world use if it can't stand up
against rather obvious attack techniques. I'm not interested in
carrying the maintenance and runtime overhead of a feature that's
only of academic value.
Looking at the real-world perspective, what covert channels do our
competitors in the space currently claim to do anything about?
This would represent the bar we need to clear at least as far as
documenting what we do (do the access constraint before anything else,
e.g.) or why we don't do things (disabling EXPLAIN, e.g.).
Cheers,
David.
--
David Fetter <david@fetter.org> http://fetter.org/
Phone: +1 415 235 3778 AIM: dfetter666 Yahoo!: dfetter
Skype: davidfetter XMPP: david.fetter@gmail.com
iCal: webcal://www.tripit.com/feed/ical/people/david74/tripit.ics
Remember to vote!
Consider donating to Postgres: http://www.postgresql.org/about/donate
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Josh Berkus <josh@agliodbs.com> writes:
That would close only one covert channel. Others were already pointed out
upthread, and I'll bet there are more ...
Mind you, fundamentally this is no different from allowing INSERT
permission on a table but denying SELECT, or denying SELECT on certain
columns. In either case, covert channels for some data are available.
Certainly. But INSERT's purpose in life is not to prevent people from
inferring what data is in the table. What we have to ask here is whether
a "row level security" feature that doesn't deal with these real-world
attack techniques is worth having.
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
2013/8/29 Josh Berkus <josh@agliodbs.com>:
Kaigai,
Although I didn't touch this task by myself, my senior colleague said that we
calculated some possible bandwidth of leakage as an evident when we ship
supercomputer system with MAC feature for customer who worry about security.
I'm not sure how to measure it. However, if we assume a human can run up to
5 query per seconds, he needs 2-3 seconds to identify a particular integer value
less than 10000, it means bandwidth of this covert channel is less than 5bps.
I'm not sure whether enterprise-grade dbms has to care about these amount
of covert channel.Why are you assuming a human needs to do it? Given the explain vector,
I could write a rather simple python or perl script which would find
values by EXPLAIN leakage, at 1000 explain plans per minute.It's one thing to day "we can't solve this covert channel issue right
now in this patch", but saying "we don't plan to solve it at all" is
likely to doom the patch.I'm not sure what the solution would be, exactly. Deny permission for
EXPLAIN on certain tables?Surely someone in the security community has discussed this?
Security community considers covert channel is a hard to solve problem;
nearly impossible to eliminate.
Let's see the summary in wikipedia:
http://en.wikipedia.org/wiki/Covert_channel
It does not require countermeasure of covert channels in middle or entry
class security evaluation; that is usually required for enterprise grade,
even though it is required for the product being designed for military
grade.
The reason why its priority is relatively lower, is that degree of threats
with information leakage via covert channel has limited bandwidth in
comparison to main channel.
I also follow this standpoint; that is enough reasonable between
functionality and its strictness under limited resources.
Even if we could close a certain channel, we never can all other
channels, like a signal by namespace contention on table creation
as covert channel. Also, I don't know major commercial dbms handles
these scenario well.
Of course, it should be described in the document for users not to
apply these security features onto the region that overs our capability.
Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
2013/8/29 David Fetter <david@fetter.org>:
On Thu, Aug 29, 2013 at 10:05:14AM -0400, Tom Lane wrote:
Alexander Korotkov <aekorotkov@gmail.com> writes:
On Wed, Aug 28, 2013 at 4:17 PM, Kohei KaiGai <kaigai@kaigai.gr.jp> wrote:
It is out of scope for this feature. We usually calls this type
of information leakage "covert channel"; that is not avoidable in
principle.I think there is another "covert channel" much more serious than
constrains. You can gather information about hidden data by
reading query plans.I'm not convinced by this argument that covert channels are "out of
scope". That would be a fine justification for, say, a thesis
topic. However, what we're talking about here is a real-world
feature that will be of no real-world use if it can't stand up
against rather obvious attack techniques. I'm not interested in
carrying the maintenance and runtime overhead of a feature that's
only of academic value.Looking at the real-world perspective, what covert channels do our
competitors in the space currently claim to do anything about?
I'm not sure whether minor dbms that is designed for extreme secure
environment already got certified. (If they have such functionality,
they should take certification for promotion.)
Oracle lists some of their certified products:
http://www.oracle.com/technetwork/topics/security/security-evaluations-099357.html
However, these are based on protection profile for basic robustness that is
designed for environment where we don't care about covert channel.
This would represent the bar we need to clear at least as far as
documenting what we do (do the access constraint before anything else,
e.g.) or why we don't do things (disabling EXPLAIN, e.g.).
+1. I'd like to add description about this scenario.
Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
2013/8/29 Tom Lane <tgl@sss.pgh.pa.us>:
Josh Berkus <josh@agliodbs.com> writes:
That would close only one covert channel. Others were already pointed out
upthread, and I'll bet there are more ...Mind you, fundamentally this is no different from allowing INSERT
permission on a table but denying SELECT, or denying SELECT on certain
columns. In either case, covert channels for some data are available.Certainly. But INSERT's purpose in life is not to prevent people from
inferring what data is in the table. What we have to ask here is whether
a "row level security" feature that doesn't deal with these real-world
attack techniques is worth having.
I think, we should clearly note that row-level security feature does not
have capability to control information leakage via covert channel but
very limited bandwidth, even though it control information leakage and
manipulation via main channel.
It depends on user's environment and expectation. If they need rdbms
with security feature for military grade, it is not recommendable.
However, it is a recommended solution for regular enterprise grade
environment. Anything depends on user's environment, threats and
worth of values to be protected.
Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 08/30/2013 03:05 AM, Kohei KaiGai wrote:
Surely someone in the security community has discussed this?
Security community considers covert channel is a hard to solve problem;
nearly impossible to eliminate.
Let's see the summary in wikipedia:
http://en.wikipedia.org/wiki/Covert_channel
Well, in each of the cases covered in that article, the given technology
(OSI, TCP, etc.) takes specific provisions to limit the ability of
attackers to discover information via the covert channel.
However, we have yet to talk about taking any such provisions with
Postgres. If we commit this patch, arguably we'll have a row-level
security feature which only protects data from well-behaved users, which
seems counterproductive.
So, arguments in favor of this patch:
a) it's as good as Oracle's security features, giving us "check-box
compliance".
b) it allows securing individual rows against attackers with limited
technical knowledge or limited database access, and could be very
hardened in combination with intelligent access control.
c) it is an improvement on techniques like Veil (is it)?
d) we plan to continue improving it and closing covert channels, or
limiting their bandwidth.
Arguments against:
m) covert channels are currently broad enough to make it trivially
circumventable (are they?)
n) overhead and code maintenance required is substantial
So, determinative questions:
1) are the security mechanisms supplied by this patch superior in some
way to approaches like Veil for multi-tenant applications? (this is a
serious question, as multi-tenant applications are far less concerned
about covert channels)
2) do we plan to reduce the accessibility of data via covert channels
over successive releases? How?
3) will accepting this patch allow our users in the Government space to
more freely adopt PostgreSQL?
--
Josh Berkus
PostgreSQL Experts Inc.
http://pgexperts.com
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Import Notes
Reply to msg id not found: WMfb94ecb91cd381e69d8e4773d3810f48e4e0ef000525cc806c4ee509f215ce1a39c8c2a1c3ffde72ba2ddb1a07ebcdc9@asav-2.01.com
Josh,
* Josh Berkus (josh@agliodbs.com) wrote:
On 08/30/2013 03:05 AM, Kohei KaiGai wrote:
Security community considers covert channel is a hard to solve problem;
nearly impossible to eliminate.
While impossible to eliminate, we should certainly consider cases like
this where we can do better and fix them. RLS certainly brings another
level of consideration to the overall PG security environment by
requiring we think about security on a row level rather than just a
table or column level.
We have issues with covert channels even without RLS though and holding
up RLS because it doesn't fix all the covert channels isn't sensible.
Column-level privleges have a similar problem, where you can read the
default value for a column using the catalogs. Perhaps the default
isn't sensetive (you'd certainly hope not), but it's still an issue. It
wouldn't surprise me to find that there are ways to abuse a multi-column
index which includes both a column you can manipulate and one you don't
have access to read to determine something about the hidden column
(maybe you have access to the 2nd field in the index and you can
encourage an in-order index traversal and then look at filtered rows, or
just work out a way to do timing attacks to determine the btree depth).
Well, in each of the cases covered in that article, the given technology
(OSI, TCP, etc.) takes specific provisions to limit the ability of
attackers to discover information via the covert channel.
The work we've done around secure views would lend credit to our
attempts at taking specific provisions as well; sadly, PG is slightly
more complicated than TCP. We do what we can and we've got a great
community which will point out where we can do better- and we work on it
and improve over time. Hell, when roles were first added we had a
*massive* security hole because we didn't check to make sure we weren't
overrunning the length of the GUC. It was a mistake and we should have
done better, but that doesn't mean adding roles was the wrong decision.
However, we have yet to talk about taking any such provisions with
Postgres. If we commit this patch, arguably we'll have a row-level
security feature which only protects data from well-behaved users, which
seems counterproductive.
I would argue both that we *have* been taking provisions to avoid
obvious and big covert channels, and that this patch adds value even
if it doesn't protect the system perfectly from malicious users. We're
all certainly aware of the ability for an attacker to cause major
problems to a PG system if they can issue arbitrary SQL and our
permissions system doesn't do much to protect us. A single query which
doesn't require any privileges could cause havok on the system (massive
on-disk temp file, which could be shared with pg_xlog causing the system
to PANIC, massive CPU load if they can execute multiple commands in
parallel...). Not to mention the default installation of pl/pgsql and
anonymous functions.
I could see many a web app (things like LedgerSMB) which could benefit
from having more fine-grained in-database control because they already
authenticate to the database as the user and have a static or at least
controlled set of queries which they run. Today, any of those kinds of
systems have to implement their own RLS (though sometimes it's done
through independent tables for each customer or similar, rather than as
conditionals added to queries).
a) it's as good as Oracle's security features, giving us "check-box
compliance".
I'd argue that this is definitely much more than 'check-box' compliance.
b) it allows securing individual rows against attackers with limited
technical knowledge or limited database access, and could be very
hardened in combination with intelligent access control.
c) it is an improvement on techniques like Veil (is it)?
d) we plan to continue improving it and closing covert channels, or
limiting their bandwidth.Arguments against:
m) covert channels are currently broad enough to make it trivially
circumventable (are they?)
There are some which are and likely deserve to be fixed. Do they all
need to be addressed before this patch goes in? I'd argue 'no'.
n) overhead and code maintenance required is substantial
So, determinative questions:
1) are the security mechanisms supplied by this patch superior in some
way to approaches like Veil for multi-tenant applications? (this is a
serious question, as multi-tenant applications are far less concerned
about covert channels)
I'd argue 'yes' if just for the fact that it'd be simpler and easier to
use, both because it's in core and because you're using an actual
grammar instead of function calls- but this RLS does more than just
that, it's going to cause us to improve things that Veil probably can't
fix and simply ignores today.
2) do we plan to reduce the accessibility of data via covert channels
over successive releases? How?
By discovering them and fixing them as we go..? I can't imagine there
being one massive patch which goes into a single major release that
fixes *all* of them- there's going to be ones we can't even imagine
today that we discover later. Should we fix *all* of the ones that we
discover? Probably not- it's simply not possible. Should we fix the
ones that we can easily correct? Of course.
3) will accepting this patch allow our users in the Government space to
more freely adopt PostgreSQL?
There's two parts to this. On the one hand, the 'check-box' would be
filled, which by itself would make it easier (at least based on my
experience w/ the US gov't, ymmv), but also because it would require
*less work* to build a new application on PG which needed RLS. You can
already do it today, but you have to bake that into the cost of the
implementation of the overall application and accept the limitations
which come with it- trivally gotten around once you get a direct
connection to PG. Would this be perfect? No, but it'd be quite a bit
better.
Thanks,
Stephen
Stephen Frost <sfrost@snowman.net> writes:
We have issues with covert channels even without RLS though and holding
up RLS because it doesn't fix all the covert channels isn't sensible.
I think it's entirely sensible to question whether we should reject (not
"hold up") RLS if it has major covert-channel problems.
The scenario I'm worried about is where somebody says "hey, Postgres has
RLS now, I can rely on that to hide my sooper sekrit data from other users
in the same database", and later they have a security breach through some
covert-channel attack. Are they going to blame themselves? No, they're
gonna blame Postgres. Or consider the case where some bozo publishes
a method for such an attack and uses it to badmouth us as insecure.
I don't think we need the headaches that will result from promising
(or at least appearing to promise) something we can't deliver. Nor am
I convinced that we're really doing users any favors by providing such a
feature. They'd be *far* better advised to put their critical data in a
separate database.
In short, "we can check some check-box" is a really, really bad reason
to accept a security-related feature. If we're going to put up with
all the downsides of RLS, I want the end result to be something that's
actually secure, not something that gives the illusion of security.
And right now, I do not believe we can get past the illusion stage,
ever (certainly not in a release or two).
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 08/30/2013 12:43 PM, Tom Lane wrote:
In short, "we can check some check-box" is a really, really bad reason
to accept a security-related feature. If we're going to put up with
all the downsides of RLS, I want the end result to be something that's
actually secure, not something that gives the illusion of security.
And right now, I do not believe we can get past the illusion stage,
ever (certainly not in a release or two).
Can you be more explicit about "all the downsides of RLS"? I was just
looking over the patch, which is less than 5000 lines. While it's not
small, we have larger patches in the CF. So what's the specific
downsides of this?
The reason I brought up multi-tenant applications ("MTA"), BTW, is that
this is the other major potential utility of RLS, and for such users the
covert channel limitations are acceptable (as long as we publish them).
That is, for a "thing-as-a-service" application, users are not assumed
to have unlimited access to the SQL command line anyway; RLS is employed
just to limit the damage they can do if they get access, and to limit
the disclosure if some app programmer makes a mistake.
Right now, the primary tool for doing row filtering for MTA is Veil,
which has numerous and well-known limitations. If RLS has fewer
limitations, or is easier to deploy, maintain, and/or understand, then
it's a valuable feature for that user base, even if it doesn't address
the covert channels we've brought up at all.
That is, if RLS is your *second* level of defense, instead of your
primary defense, covert channels are not a make-or-break issue. It just
has to be better than what we had before.
Note that I have NOT done an evaluation of Veil vs. RLS for MTA at this
point. I'm hoping someone else will ;-)
--
Josh Berkus
PostgreSQL Experts Inc.
http://pgexperts.com
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Import Notes
Reply to msg id not found: WM20618a88b943d2d51dde90beb5cb85eea4bacc1ee65b4bb73872fafa086f76aee8b9caf4e1c1a7e5865845cf76661b1b@asav-3.01.com
* Tom Lane (tgl@sss.pgh.pa.us) wrote:
Stephen Frost <sfrost@snowman.net> writes:
We have issues with covert channels even without RLS though and holding
up RLS because it doesn't fix all the covert channels isn't sensible.I think it's entirely sensible to question whether we should reject (not
"hold up") RLS if it has major covert-channel problems.
Rejecting RLS because we've suddently realized that covert channels
exist is foolishness. It's akin to rejecting the ability to add stored
procedures because we don't protect prosrc from people who don't own or
can't execute the function.
The scenario I'm worried about is where somebody says "hey, Postgres has
RLS now, I can rely on that to hide my sooper sekrit data from other users
in the same database", and later they have a security breach through some
covert-channel attack. Are they going to blame themselves? No, they're
gonna blame Postgres. Or consider the case where some bozo publishes
a method for such an attack and uses it to badmouth us as insecure.
In my experience, issues are discovered, patched, and released as
security updates. Does adding RLS mean we might have more of those?
Sure, as did column level privileges and as does *any* such increase in
the granularity of what we can provide security-wise.
I don't think we need the headaches that will result from promising
(or at least appearing to promise) something we can't deliver. Nor am
I convinced that we're really doing users any favors by providing such a
feature. They'd be *far* better advised to put their critical data in a
separate database.
We've barely got cross-database queries with FDWs. The notion that
adding such complexity into those as RLS, which each individual user
will need to figure out how to do themselves and most will likely get
far wrong and much worse than what we'd implement, is "better" for
our users is just ridiculous.
In short, "we can check some check-box" is a really, really bad reason
to accept a security-related feature. If we're going to put up with
all the downsides of RLS, I want the end result to be something that's
actually secure, not something that gives the illusion of security.
And right now, I do not believe we can get past the illusion stage,
ever (certainly not in a release or two).
I'm not argueing for this because it fulfills some check-box; the
question about if it would help a given set of clients (ones which I no
longer have any direct relationship with, as it turns out) adopt PG was
asked and I answered it as best I could.
I certainly think we need to get past the 'illusion' stage also. I'm
certainly more optimistic about that than you are but I also understand
it's not going to be perfect in the first release- but I do think it'll
be better than the 'illusion' stage. It'll get there because we'll
continue to discuss it, people will test it, etc; as one hopes happens
with all new features, but this even more than others.
Thanks,
Stephen
Josh Berkus <josh@agliodbs.com> writes:
On 08/30/2013 12:43 PM, Tom Lane wrote:
In short, "we can check some check-box" is a really, really bad reason
to accept a security-related feature. If we're going to put up with
all the downsides of RLS, I want the end result to be something that's
actually secure, not something that gives the illusion of security.
Can you be more explicit about "all the downsides of RLS"? I was just
looking over the patch, which is less than 5000 lines. While it's not
small, we have larger patches in the CF. So what's the specific
downsides of this?
I think it's going to be an ongoing maintenance headache and an endless
source of security bugs, even disregarding covert-channel issues. I have
pretty much zero faith in the planner changes, in particular, and would
still not have a lot if they were adequately documented, which they
absolutely are not. The whole thing depends on nowhere-clearly-stated
assumptions that plan-time transformations will never allow an RLS check
to be bypassed. I foresee future planner work breaking this in
non-obvious ways on a regular basis (even granting the assumption that
it's bulletproof today, which is at best unproven).
The reason I brought up multi-tenant applications ("MTA"), BTW, is that
this is the other major potential utility of RLS, and for such users the
covert channel limitations are acceptable (as long as we publish them).
[ shrug... ] You might've noticed I work for a multi-tenant shop now.
I'm still not excited about this.
That is, if RLS is your *second* level of defense, instead of your
primary defense, covert channels are not a make-or-break issue. It just
has to be better than what we had before.
Yeah, that's a fair point. I'm just not convinced that it's enough better
to justify the maintenance burden we'll be taking on. I'm not thrilled
about the "every bug is a security bug" angle, either.
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
2013/8/30 Josh Berkus <josh@agliodbs.com>:
On 08/30/2013 03:05 AM, Kohei KaiGai wrote:
Surely someone in the security community has discussed this?
Security community considers covert channel is a hard to solve problem;
nearly impossible to eliminate.
Let's see the summary in wikipedia:
http://en.wikipedia.org/wiki/Covert_channelWell, in each of the cases covered in that article, the given technology
(OSI, TCP, etc.) takes specific provisions to limit the ability of
attackers to discover information via the covert channel.However, we have yet to talk about taking any such provisions with
Postgres. If we commit this patch, arguably we'll have a row-level
security feature which only protects data from well-behaved users, which
seems counterproductive.
The point we shouldn't forget is information leakage via covert-channel
has less grade of threat than leakage via main-channel, because of
much less bandwidth.
Security community also concludes it is not avoidable nature as long
as human can observe system behavior and estimate something, thus,
security evaluation criteria does not require eliminate covert-channels
or does not pay attention about covert-channels for the products that
is installed on the environment with basic robustness (that means,
non-military, regular enterprise usage).
I don't think PostgreSQL goes into military-grade secure database
system. If so, it has to sacrifice many thing (like, performance,
usability, nature of open source, ...) for security. It's not a fact.
So, arguments in favor of this patch:
a) it's as good as Oracle's security features, giving us "check-box
compliance".
b) it allows securing individual rows against attackers with limited
technical knowledge or limited database access, and could be very
hardened in combination with intelligent access control.
Even if attacker has enough knowledge, the ratio they can estimate
hidden values is very limited because of much less bandwidth of
covert channels.
c) it is an improvement on techniques like Veil (is it)?
d) we plan to continue improving it and closing covert channels, or
limiting their bandwidth.Arguments against:
m) covert channels are currently broad enough to make it trivially
circumventable (are they?)
n) overhead and code maintenance required is substantialSo, determinative questions:
1) are the security mechanisms supplied by this patch superior in some
way to approaches like Veil for multi-tenant applications? (this is a
serious question, as multi-tenant applications are far less concerned
about covert channels)
Yes. This RLS implementation targets the environment that does not
have requirement for a particular bandwidth upperbound on covert-
channels. It allows to centralize the place where we have to care
about access control on main-channel, so it well fits multi-tenant
applications.
2) do we plan to reduce the accessibility of data via covert channels
over successive releases? How?
Less covert channels is better than massive, if we could close it with
reasonable cost; that means run-time performance, code complexity
and so on.
However, in general, it is impossible to eliminate anything in spite of
less degree of threats because of smaller bandwidth. So, I don't think
this is an issue to spent much efforts to solve.
3) will accepting this patch allow our users in the Government space to
more freely adopt PostgreSQL?
Government has two spaces. Most of their environment requires similar
requirement as enterprise grade system doing. On the other hand,
military environment often requires upper-bound of covert channel,
as a story I introduce on upthread, but they are minority and have
budget to develop special purpose database system designed for
security with first priority.
Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
2013/8/30 Tom Lane <tgl@sss.pgh.pa.us>:
Josh Berkus <josh@agliodbs.com> writes:
On 08/30/2013 12:43 PM, Tom Lane wrote:
In short, "we can check some check-box" is a really, really bad reason
to accept a security-related feature. If we're going to put up with
all the downsides of RLS, I want the end result to be something that's
actually secure, not something that gives the illusion of security.Can you be more explicit about "all the downsides of RLS"? I was just
looking over the patch, which is less than 5000 lines. While it's not
small, we have larger patches in the CF. So what's the specific
downsides of this?I think it's going to be an ongoing maintenance headache and an endless
source of security bugs, even disregarding covert-channel issues. I have
pretty much zero faith in the planner changes, in particular, and would
still not have a lot if they were adequately documented, which they
absolutely are not. The whole thing depends on nowhere-clearly-stated
assumptions that plan-time transformations will never allow an RLS check
to be bypassed. I foresee future planner work breaking this in
non-obvious ways on a regular basis (even granting the assumption that
it's bulletproof today, which is at best unproven).
In general, we will adopt / enhance features as long as PostgreSQL runs
with evolution. It can never be free from bugs or maintenance, regardless
of its nature.
Later half seems to me a bit unfair because any features may conflict
with some future works, not only RLS. Also, if you have some tangible
planner enhancement plan, could you inform us which plan shall be
in progress? At least, it is impossible to adjust my implementation
because of abstract concern towards future conflict.
Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
KaiGai,
* Kohei KaiGai (kaigai@kaigai.gr.jp) wrote:
The point we shouldn't forget is information leakage via covert-channel
has less grade of threat than leakage via main-channel, because of
much less bandwidth.
While true, this argument can't be used to simply ignore any and all
covert channels. There are covert channels which are *not* much less
bandwidth, and the Filtered Rows one is one of those- it's simply too
big to ignore. There are likely other which are equally large and
until we clean those up our RLS implementation will be questioned by
our users.
This does not mean that we need to clean up all covert channels and
things which are clearly intractable don't need to be addressed (eg:
the unique constraint situation; we can't both guarantee uniqueness
and hide the existance of an entry).
Security community also concludes it is not avoidable nature as long
as human can observe system behavior and estimate something,
This particular case is actually beyond 'estimation'.
Even if attacker has enough knowledge, the ratio they can estimate
hidden values is very limited because of much less bandwidth of
covert channels.
You really need to back away from this argument in this case. The
example shown isn't based on estimation and provides quite high
bandwidth because it can be run by a computer. This argument can't
be used for every situation where information is leaked.
However, in general, it is impossible to eliminate anything in spite of
less degree of threats because of smaller bandwidth. So, I don't think
this is an issue to spent much efforts to solve.
I don't see a lot of complexity required to fix this specific case. A
great deal of effort will be involved in going through the rest of the
code and various options and working to eliminate other similar cases,
but that's a reality we need to deal with. Hopefully the cases found
will not require a lot of additional code to deal with. We will need to
be mindful of new code which adds leaks or changes behavior such that
RLS doesn't function properly (hopefully, sufficient regression tests
will help to address that as well).
Thanks,
Stephen
I think there is another "covert channel" much more serious than
constrains. You can gather information about hidden data by reading
query plans.
I think a lot of people would be quite happy to simply disallow EXPLAIN.
Define a permission for it, grant it to public and newly created
users/groups by default (for BC), and allow it to be revoked.
To define what is/isn't reasonable in terms of covert channel leakage, I
(reluctantly) suggest checking out the Common Criteria stuff. Yes, it's
verbose and questionably useful, but it's something that already exists
and that has lots of weight in government / large orgs. I've started
reading into it but don't have enough info to comment on RLS
specifically yet.
Regarding unique keys and other constraints as leakage channels, I'm
inclined to think that this is partly a documentation issue. The docs
can explain what is/isn't protected against. Suggest creating keys
incorporating security domain identifiers so that users in different
security domains can't create conflicting values.
Possibly provide a mechanism to enforce that so that users can't attempt
to insert/update rows with a different security identifier. Or even make
it transparently part of the system's operations, an implicit extra
column in PRIMARY and FOREIGN keys on RLS-enabled tables.
What I'm more worried about re the covert uniqueness issue is that any
solution might run the risk of creating situations where changes in
access rights can make previously valid data in tables invalid. If two
users A and B can't see each other's data and create the same values for
a key, then B is given the rights to see A's data ... those unique
values now have duplicates.
So: I do think we need to step back a little when it comes to covert
channel attacks and define what we do/don't defend against.
What about timing attacks - do we need to make sure we don't leak
information about the number of RLS rows via scan durations? Make
everything constant-time? Yick. Crypto systems have a hard enough time
getting this stuff right, and I just think we say we don't defend
against timing attacks. If someone wants to mess around with adding
random sleeps in an executor hook, well, that's their pain to deal with.
--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 30.08.2013 22:57, Josh Berkus wrote:
Right now, the primary tool for doing row filtering for MTA is Veil,
which has numerous and well-known limitations. If RLS has fewer
limitations, or is easier to deploy, maintain, and/or understand, then
it's a valuable feature for that user base, even if it doesn't address
the covert channels we've brought up at all.That is, if RLS is your*second* level of defense, instead of your
primary defense, covert channels are not a make-or-break issue. It just
has to be better than what we had before.Note that I have NOT done an evaluation of Veil vs. RLS for MTA at this
point. I'm hoping someone else will ;-)
I'd also like to hear how Veil differs from RLS. From what I've
understood this far, they are the same in terms of what you can and
cannot do.
To phrase it differently: We already have RLS. It's shipped as an
extension called Veil. Now please explain what's wrong with that
statement, if anything.
- Heikki
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 2013-09-01 16:38:51 +0300, Heikki Linnakangas wrote:
On 30.08.2013 22:57, Josh Berkus wrote:
Right now, the primary tool for doing row filtering for MTA is Veil,
which has numerous and well-known limitations. If RLS has fewer
limitations, or is easier to deploy, maintain, and/or understand, then
it's a valuable feature for that user base, even if it doesn't address
the covert channels we've brought up at all.That is, if RLS is your*second* level of defense, instead of your
primary defense, covert channels are not a make-or-break issue. It just
has to be better than what we had before.Note that I have NOT done an evaluation of Veil vs. RLS for MTA at this
point. I'm hoping someone else will ;-)I'd also like to hear how Veil differs from RLS. From what I've understood
this far, they are the same in terms of what you can and cannot do.To phrase it differently: We already have RLS. It's shipped as an extension
called Veil. Now please explain what's wrong with that statement, if
anything.
I don't think veil really is an argument for much in this discussion. I
don't know who in this discussion has used it, I have. Admittedly a good
bit ago, 8.2, 8.3 days I think. There hasn't been much fundamental
development since though, if my quick look is right.
Veil gives you a rather low level toolbox for developing an RLS and
you're left with a *huge* amount of work needed ontop.
It basically gives you a bunch of new datatypes (sets, bitmaps, ranges) and
provides some support for sharing variables across backends (shared
memory). That's nearly it.
Greetings,
Andres Freund
--
Andres Freund http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 9/1/13 9:38 AM, Heikki Linnakangas wrote:
To phrase it differently: We already have RLS. It's shipped as an
extension called Veil. Now please explain what's wrong with that
statement, if anything.
Veil was last updated for 9.1 to work against that version, so the first
thing is that it's two versions back from being current.
The main improvement for a few now core features, compared to their
external/extension predecessors, is that they go through a real review
process. I suspect a lot of the criticisms being lobbied against the
core RLS feature would also hit Veil if it were evaluated to the same
standard.
Regardless, I'm seeing a few review themes pop up from this thread:
-Comparison against the Veil feature set.
-Competitive review against industry expectations, AKA "checkbox"
compliance.
-Confirm feature set is useful to government security clearance
applications and multi-tenant applications. There's also a secured web
application use case that's popped up a few times too; KaiGai has used
secured Apache installs for example.
-Summary of known covert channels, with documentation coverage.
-Assess odds of this implementation's future issues turning into
security bugs. My personal hotspot here is that I'd like minimal code
exposure to people who don't use this feature at all. Are there parts
here that should be compile time enabled?
Of course those are all on top of the usual code quality review. Did I
miss any big themes on that list?
--
Greg Smith 2ndQuadrant US greg@2ndQuadrant.com Baltimore, MD
PostgreSQL Training, Services, and 24x7 Support www.2ndQuadrant.com
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Kaigai,
However, we have yet to talk about taking any such provisions with
Postgres. If we commit this patch, arguably we'll have a row-level
security feature which only protects data from well-behaved users, which
seems counterproductive.The point we shouldn't forget is information leakage via covert-channel
has less grade of threat than leakage via main-channel, because of
much less bandwidth.
That's an astonishingly weak argument, because the bandwidth you're
talking about is still very high, as in *hundreds* or *thousands* of
values per minute. It's one thing to make the bandwidth argument for
things like monitoring power draw, where the leakage is one character
per hour, but the covert channels we're talking about are several orders
of magnitude faster than that.
So, I repeat: if you continue to pursue the argument that the covert
channels identified aren't significant because of bandwidth, you will
doom this patch. The valid arguments are only two:
a) clearly identified use case X doesn't care about covert channels for
reason Y, and will find RLS extremely useful.
b) we can't fix these covert channels now, but plan to work on them in
the future, and have ideas for how to approach them.
Security community also concludes it is not avoidable nature as long
as human can observe system behavior and estimate something, thus,
security evaluation criteria does not require eliminate covert-channels
or does not pay attention about covert-channels for the products that
is installed on the environment with basic robustness (that means,
non-military, regular enterprise usage).
To be completely blunt, the security community does not understand
databases. At all. I'd think if anything had become clear through the
course of work on SEPosgres, it would be that.
So, determinative questions:
1) are the security mechanisms supplied by this patch superior in some
way to approaches like Veil for multi-tenant applications? (this is a
serious question, as multi-tenant applications are far less concerned
about covert channels)Yes. This RLS implementation targets the environment that does not
have requirement for a particular bandwidth upperbound on covert-
channels. It allows to centralize the place where we have to care
about access control on main-channel, so it well fits multi-tenant
applications.
Again, please abandon your bandwidth arguments. They are irrelevant to
whether or not this patch gets accepted.
So, please try again?
3) will accepting this patch allow our users in the Government space to
more freely adopt PostgreSQL?Government has two spaces. Most of their environment requires similar
requirement as enterprise grade system doing. On the other hand,
military environment often requires upper-bound of covert channel,
as a story I introduce on upthread, but they are minority and have
budget to develop special purpose database system designed for
security with first priority.
I don't think I understood this answer. What I'm asking is: will
government agencies be sigificantly more likely to adopt PostgreSQL if
we have RLS, in this form? Or do we need "military-grade" before it
makes a difference?
--
Josh Berkus
PostgreSQL Experts Inc.
http://pgexperts.com
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Import Notes
Reply to msg id not found: WM12ab40873dea1bdff46421b93993af10c7940c289ac32373a30649be9839739492ccb794e20a8b018ced351a2e740d55@asav-3.01.com
2013/8/31 Stephen Frost <sfrost@snowman.net>:
KaiGai,
* Kohei KaiGai (kaigai@kaigai.gr.jp) wrote:
The point we shouldn't forget is information leakage via covert-channel
has less grade of threat than leakage via main-channel, because of
much less bandwidth.While true, this argument can't be used to simply ignore any and all
covert channels. There are covert channels which are *not* much less
bandwidth, and the Filtered Rows one is one of those- it's simply too
big to ignore. There are likely other which are equally large and
until we clean those up our RLS implementation will be questioned by
our users.This does not mean that we need to clean up all covert channels and
things which are clearly intractable don't need to be addressed (eg:
the unique constraint situation; we can't both guarantee uniqueness
and hide the existance of an entry).Security community also concludes it is not avoidable nature as long
as human can observe system behavior and estimate something,This particular case is actually beyond 'estimation'.
Even if attacker has enough knowledge, the ratio they can estimate
hidden values is very limited because of much less bandwidth of
covert channels.You really need to back away from this argument in this case. The
example shown isn't based on estimation and provides quite high
bandwidth because it can be run by a computer. This argument can't
be used for every situation where information is leaked.However, in general, it is impossible to eliminate anything in spite of
less degree of threats because of smaller bandwidth. So, I don't think
this is an issue to spent much efforts to solve.I don't see a lot of complexity required to fix this specific case. A
great deal of effort will be involved in going through the rest of the
code and various options and working to eliminate other similar cases,
but that's a reality we need to deal with. Hopefully the cases found
will not require a lot of additional code to deal with. We will need to
be mindful of new code which adds leaks or changes behavior such that
RLS doesn't function properly (hopefully, sufficient regression tests
will help to address that as well).
You're saying that we need to fix up cover-channels with unignorable
threat degree, even though it does not mean elimination of all the
covert-channels. I almost agree and feel it a reasonable stance.
One thing we need to pay attention is, how large covert-channel is
we need to fix-up and how small is we can ignore.
The reason why I used the term of "estimation", even heuristic or
machinery way, is this covert-channel does not expose the raw
data on the display.
I (personally) used this criteria to decide whether the covert-channel
is ignorable, or not. Thus, I tackled leaky-function problem on interaction
with views for security purpose.
It seems to me a rough watermark, rather than my criteria above, is
necessary to determine which covert-channel is hit for us.
Anyway, I try to investigate the scenario that Korotkov pointed out.
It should be a common problem for views with security_barrier attribute,
not only RLS feature, because it is designed on a common infrastructure.
Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Josh,
* Josh Berkus (josh@agliodbs.com) wrote:
That's an astonishingly weak argument, because the bandwidth you're
talking about is still very high, as in *hundreds* or *thousands* of
values per minute.
I agree that, in this specific case, we should do something about it.
It's one thing to make the bandwidth argument for
things like monitoring power draw, where the leakage is one character
per hour, but the covert channels we're talking about are several orders
of magnitude faster than that.
That's actually not an accurate representation of the bandwidth
associated with power draw measurements, but even if I convinced you it
was in the hundreds or thousands of values per minute, I doubt you'd
change your mind regarding the Filtered Rows issue or claim that we
should fix the power draw measurement issue. :)
So, I repeat: if you continue to pursue the argument that the covert
channels identified aren't significant because of bandwidth, you will
doom this patch. The valid arguments are only two:
There is actually another argument, which I mentioned up-thread.. There
are cases where you can't guarantee all the promises which can be asked
for. In the unique-value case, we can't guarantee both that the values
are unique and that existing values can't be detected by an individual
with insert access. We should make the user aware that allowing insert
access (or perhaps 'explain') opens up these cases.
a) clearly identified use case X doesn't care about covert channels for
reason Y, and will find RLS extremely useful.
These certainly exist and I'd argue that's part of why Veil exists..
Users of Veil (which I admit, I'm not) would likely be much happier
with an in-core solution because it will be much easier to use and
much more performant.
b) we can't fix these covert channels now, but plan to work on them in
the future, and have ideas for how to approach them.
I expect we will find more covert chanels which need to be fixed.
To be completely blunt, the security community does not understand
databases. At all. I'd think if anything had become clear through the
course of work on SEPosgres, it would be that.
That's just false and it's poor of you to claim it. The security
community is not one individual nor even some small group. They're not
very obviously involved with PostgreSQL but that's no reason to claim
that they do not understand databases.
I don't think I understood this answer. What I'm asking is: will
government agencies be sigificantly more likely to adopt PostgreSQL if
we have RLS, in this form? Or do we need "military-grade" before it
makes a difference?
From my experience, the answer would be 'yes' to your first question.
Thanks,
Stephen
2013/9/1 Josh Berkus <josh@agliodbs.com>:
Kaigai,
However, we have yet to talk about taking any such provisions with
Postgres. If we commit this patch, arguably we'll have a row-level
security feature which only protects data from well-behaved users, which
seems counterproductive.The point we shouldn't forget is information leakage via covert-channel
has less grade of threat than leakage via main-channel, because of
much less bandwidth.That's an astonishingly weak argument, because the bandwidth you're
talking about is still very high, as in *hundreds* or *thousands* of
values per minute. It's one thing to make the bandwidth argument for
things like monitoring power draw, where the leakage is one character
per hour, but the covert channels we're talking about are several orders
of magnitude faster than that.So, I repeat: if you continue to pursue the argument that the covert
channels identified aren't significant because of bandwidth, you will
doom this patch. The valid arguments are only two:a) clearly identified use case X doesn't care about covert channels for
reason Y, and will find RLS extremely useful.b) we can't fix these covert channels now, but plan to work on them in
the future, and have ideas for how to approach them.
An important point is, what covert-channel should be closed, and what
other covert-channel can be ignored.
Even though the scenario Korotkov reported would be enough to make
us surprised, we may need to consider how wide covert channel should
be closed soon or later.
* A covert-channel that can expose raw-data directly, should be fixed up.
=> Thus, leaky-view problem needed to be fixed.
* A covert-channel that can expose existence of a particular value with
less than Log(N) order trials to the possible range of hidden value.
=> Thus, this scenario needs to be cared?
Or, any other criteria even though?
My (current) preference is plan (c: we will be able to fix up *this*
cover-channel with reasonable efforts on explain code. probably,
we can close it if we don't print filtered rows under the sub-query
with security-barrier attribute.
It does not mean we shall or can fix up any possible covert-channels
being found in the future, however, I also agree that we shall tackle
covert-channel scenario being enough serious; that's criteria may
or may not be bandwidth of information leakage.
Security community also concludes it is not avoidable nature as long
as human can observe system behavior and estimate something, thus,
security evaluation criteria does not require eliminate covert-channels
or does not pay attention about covert-channels for the products that
is installed on the environment with basic robustness (that means,
non-military, regular enterprise usage).To be completely blunt, the security community does not understand
databases. At all. I'd think if anything had become clear through the
course of work on SEPosgres, it would be that.So, determinative questions:
1) are the security mechanisms supplied by this patch superior in some
way to approaches like Veil for multi-tenant applications? (this is a
serious question, as multi-tenant applications are far less concerned
about covert channels)Yes. This RLS implementation targets the environment that does not
have requirement for a particular bandwidth upperbound on covert-
channels. It allows to centralize the place where we have to care
about access control on main-channel, so it well fits multi-tenant
applications.Again, please abandon your bandwidth arguments. They are irrelevant to
whether or not this patch gets accepted.So, please try again?
3) will accepting this patch allow our users in the Government space to
more freely adopt PostgreSQL?Government has two spaces. Most of their environment requires similar
requirement as enterprise grade system doing. On the other hand,
military environment often requires upper-bound of covert channel,
as a story I introduce on upthread, but they are minority and have
budget to develop special purpose database system designed for
security with first priority.I don't think I understood this answer. What I'm asking is: will
government agencies be sigificantly more likely to adopt PostgreSQL if
we have RLS, in this form? Or do we need "military-grade" before it
makes a difference?
Even though I'm not a marketer for public sector, not a small number
of Oracle virtual private database, that provide more simple functionality,
has been accepted for public sectors also, I believe it encourages
adoption of PostgreSQL on government agencies.
However, military division often requires special treatment for security
functionality but very small number of users are interested in, thus,
it is not a good idea to focus on this grade here.
Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Sun, Sep 1, 2013 at 8:31 PM, Kohei KaiGai <kaigai@kaigai.gr.jp> wrote:
Or, any other criteria even though?
My (current) preference is plan (c: we will be able to fix up *this*
cover-channel with reasonable efforts on explain code. probably,
we can close it if we don't print filtered rows under the sub-query
with security-barrier attribute.
I think the criteria being discussed in this thread are too strict.
It may be the case that Postgres cannot make a strong general case
that it protects against covert channels. However it may still be able
to make the much weaker case that it is *possible* to arrange your
database such that the covert channels are kept under control.
So I think up above Tom is wrong about why it's ok that INSERT leaks
keys when it reports a unique key violation. The reason why it's ok
that there's a covert channel there is that the DBA can avoid that
covert channel by being careful when creating unique constraints. He
or she should be aware that creating a unique constraint implicitly
provides a kind of limited access to data to users who have INSERT
privilege even if they lack the real SELECT privilege.
Likewise, as long as the covert channels in RLS are things the DBA has
even a modicum of control over I wouldn't be too worried. Afaict from
skimming the thread it looks like creating any indexes on columns
leaks what values of the index key exist in the table. Is it the case
that non-indexed columns do not get leaked?
--
greg
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 9/1/13 5:54 PM, Greg Stark wrote:
So I think up above Tom is wrong about why it's ok that INSERT leaks
keys when it reports a unique key violation. The reason why it's ok
that there's a covert channel there is that the DBA can avoid that
covert channel by being careful when creating unique constraints. He
or she should be aware that creating a unique constraint implicitly
provides a kind of limited access to data to users who have INSERT
privilege even if they lack the real SELECT privilege.
And if someone can INSERT values that they can't actually see once
they're committed, that's a similarly bad we should describe. People
should be dumping their trash in their neighbor's yard. I think
eventually this needs to be wrestled to the ground in a robust way. I
want to see if all unique violations might be changed to give less
information in this sort of RLS context.
One rough early idea is to create a new error condition that means you
hit something protected by RLS, but doesn't leak any more information
than that. Just a generic "Security restriction operation" that comes
out of fishing for keys, inserting outside your area, etc. I want to
think through some use cases and review the code to see whether that
concept helps or not.
--
Greg Smith 2ndQuadrant US greg@2ndQuadrant.com Baltimore, MD
PostgreSQL Training, Services, and 24x7 Support www.2ndQuadrant.com
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Sun, Sep 1, 2013 at 11:05:58AM -0700, Josh Berkus wrote:
Security community also concludes it is not avoidable nature as long
as human can observe system behavior and estimate something, thus,
security evaluation criteria does not require eliminate covert-channels
or does not pay attention about covert-channels for the products that
is installed on the environment with basic robustness (that means,
non-military, regular enterprise usage).To be completely blunt, the security community does not understand
databases. At all. I'd think if anything had become clear through the
course of work on SEPosgres, it would be that.
Agreed. The security community realizes these covert channels exist,
but doesn't really have any recommendations on how to avoid them. You
could argue that avoiding them is too tied to specific database
implementations, but there are general channels, like insert with a
unique key, that should at least have well-defined solutions.
--
Bruce Momjian <bruce@momjian.us> http://momjian.us
EnterpriseDB http://enterprisedb.com
+ It's impossible for everything to be true. +
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Fri, Aug 30, 2013 at 04:24:06PM -0400, Stephen Frost wrote:
The scenario I'm worried about is where somebody says "hey, Postgres has
RLS now, I can rely on that to hide my sooper sekrit data from other users
in the same database", and later they have a security breach through some
covert-channel attack. Are they going to blame themselves? No, they're
gonna blame Postgres. Or consider the case where some bozo publishes
a method for such an attack and uses it to badmouth us as insecure.In my experience, issues are discovered, patched, and released as
security updates. Does adding RLS mean we might have more of those?
Sure, as did column level privileges and as does *any* such increase in
the granularity of what we can provide security-wise.
Releasing a feature we think is perfect, and finding out later is isn't,
and fixing it, is not the same as releasing a feature we _know_ isn't
perfect, and having no idea how to fix it.
From later discussions, it seems like we have to accept that we will
never be able to avoid all convert channels, e.g. timing queries, but we
can avoid the most obvious ones, e.g. EXPLAIN, and improve it as we go.
Knowing there is no end to improving it does make some people feel
uncomfortable, and I can't think of another feature we have added with
such an open-ended nature. We do have open-ended performance features,
but here is seems the value of the feature itself, security, is not
attainable. Perhaps reasonable security is attainable.
I am not saying we should reject this feature --- just that the calculus
of how we decide to add this feature doesn't fit our normal analysis.
--
Bruce Momjian <bruce@momjian.us> http://momjian.us
EnterpriseDB http://enterprisedb.com
+ It's impossible for everything to be true. +
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Fri, Aug 30, 2013 at 3:43 PM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
I think it's entirely sensible to question whether we should reject (not
"hold up") RLS if it has major covert-channel problems.
We've already had this argument before, about the security_barrier
view stuff, and that code got committed and is already released. So
the horse is already out of the barn and no amount of wishing will put
it back in. I haven't reviewed this patch in a long time, but I would
expect that it's basically just reusing that same infrastructure; in
fact, I'd expect that it's little more than syntactic sugar around
that infrastructure. (If it it's instead introducing a whole new
mechanism, then I think that's reason enough to reject it right
there.) My main question about this is whether that syntactic sugar
is really worth having given that it doesn't add any real new
functionality, not about the covert channel issues, which are already
a done deal.
And frankly, I'm with the group that says the covert channel issues
are not really a big deal. In many real-world cases, the user can
control only the values that get subbed into queries that get sent to
the database, not the queries themselves, which eliminates a large
category of attacks. Real-world example, from last job: sales reps
only get to see their own accounts, not accounts of other sales reps.
They could input new accounts (with sales_rep_id set to their ID) and
they could query the list of accounts (limited to those where
sales_rep_id matched their ID) - pulling either all of them or
searching by account name, both through a web application. Yeah, a
sales rep could have launched a timing attack through the web
interface, and they could also have polled for the existence of
accounts by trying to create accounts with names that might already
exist in the system to see whether a duplicate got flagged. But
neither attack had enough useful bandwidth to matter; a sales rep
wishing to learn our full account list (so that he could try to poach
them after leaving the company) could have learned a lot more a lot
faster via social engineering, and with less risk of being caught
doing something that would have resulted in his or her immediate
termination.
The point is, I don't think RLS needs to solve every problem. What it
needs to do is solve one problem well, so that it can be used in
combination with other techniques to achieve a certain set of security
objectives. If, in a particular environment, EXPLAIN is an issue,
then it can be blocked in that environment via a variety of
well-understood techniques. That's not very hard to do even without
core support, and if we want to add core support, fine, but that's a
separate patch. This is a patch to add row-level security, and it
deserves to be judged on how well or poorly it does that, not on
whether it solves covert channel problems that represent a mostly
orthogonal set of technical issues.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Robert Haas <robertmhaas@gmail.com> writes:
On Fri, Aug 30, 2013 at 3:43 PM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
I think it's entirely sensible to question whether we should reject (not
"hold up") RLS if it has major covert-channel problems.
We've already had this argument before, about the security_barrier
view stuff, and that code got committed and is already released. So
the horse is already out of the barn and no amount of wishing will put
it back in.
Well, the security-barrier view stuff did not present itself as a 100%
solution. But perhaps more to the point, it was conceptually simple to
implement, ie don't flatten views if they have this bit set, and don't
push down quals into such sub-selects unless they're marked leakproof.
I haven't reviewed this patch in a long time, but I would
expect that it's basically just reusing that same infrastructure; in
fact, I'd expect that it's little more than syntactic sugar around
that infrastructure.
I've not read it in great detail, but it isn't that. It's whacking the
planner around in ways that I have no confidence in, and probably still
wouldn't have any confidence in if they'd been adequately documented.
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Sun, Sep 1, 2013 at 11:47 PM, Greg Smith <greg@2ndquadrant.com> wrote:
And if someone can INSERT values that they can't actually see once they're
committed, that's a similarly bad we should describe.
This is desirable in some cases but not others. If the goal is
compartmentalization, then it's sensible to prevent this. But you
might also have a "drop-box" environment - e.g. a student submits
coursework to a professor, and can't access the submitted work after
it's submitted. FWIW, my CS classes in college had a tool that worked
just this way.
Or maybe an analyst writes a report and is then permitted to "give
away" the document to his boss for revisions. Once the ownership of
the document has changed, the analyst can't see it any more, because
he can only see the documents he owns. And maybe he's not permitted
to give away documents to just anyone (polluting their sandbox), but
he can give them to his boss (who expects to receive them).
The point is that we should be in the business of providing mechanism,
not policy.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Wed, Sep 4, 2013 at 10:45 AM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
Well, the security-barrier view stuff did not present itself as a 100%
solution. But perhaps more to the point, it was conceptually simple to
implement, ie don't flatten views if they have this bit set, and don't
push down quals into such sub-selects unless they're marked leakproof.
Right. IMHO, this new feature should be similarly simple: when an
unprivileged user references a table, treat that as a reference to a
leakproof view over the table, with the RLS qual injected into the
view.
I haven't reviewed this patch in a long time, but I would
expect that it's basically just reusing that same infrastructure; in
fact, I'd expect that it's little more than syntactic sugar around
that infrastructure.I've not read it in great detail, but it isn't that. It's whacking the
planner around in ways that I have no confidence in, and probably still
wouldn't have any confidence in if they'd been adequately documented.
If that's the case, then I agree that we should not accept it, at
least in its present form.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Robert Haas <robertmhaas@gmail.com> writes:
On Wed, Sep 4, 2013 at 10:45 AM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
Well, the security-barrier view stuff did not present itself as a 100%
solution. But perhaps more to the point, it was conceptually simple to
implement, ie don't flatten views if they have this bit set, and don't
push down quals into such sub-selects unless they're marked leakproof.
Right. IMHO, this new feature should be similarly simple: when an
unprivileged user references a table, treat that as a reference to a
leakproof view over the table, with the RLS qual injected into the
view.
And for insert/update/delete, we do what exactly?
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
* Robert Haas (robertmhaas@gmail.com) wrote:
On Sun, Sep 1, 2013 at 11:47 PM, Greg Smith <greg@2ndquadrant.com> wrote:
And if someone can INSERT values that they can't actually see once they're
committed, that's a similarly bad we should describe.This is desirable in some cases but not others. If the goal is
compartmentalization, then it's sensible to prevent this. But you
might also have a "drop-box" environment - e.g. a student submits
coursework to a professor, and can't access the submitted work after
it's submitted. FWIW, my CS classes in college had a tool that worked
just this way.
Agreed, and part of the discussion that I had w/ KaiGai and Simon was
that we should provide a way to let the user pick which they'd like.
This is the concept around 'insert privileges' being different from
'select privileges' wrt RLS.
The point is that we should be in the business of providing mechanism,
not policy.
++
Thanks,
Stephen
On Wed, Sep 4, 2013 at 10:50 AM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
Robert Haas <robertmhaas@gmail.com> writes:
On Wed, Sep 4, 2013 at 10:45 AM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
Well, the security-barrier view stuff did not present itself as a 100%
solution. But perhaps more to the point, it was conceptually simple to
implement, ie don't flatten views if they have this bit set, and don't
push down quals into such sub-selects unless they're marked leakproof.Right. IMHO, this new feature should be similarly simple: when an
unprivileged user references a table, treat that as a reference to a
leakproof view over the table, with the RLS qual injected into the
view.And for insert/update/delete, we do what exactly?
The same mechanism will prevent UPDATE and DELETE from seeing any rows
the user shouldn't be able to touch.
Simon and Greg are arguing that when an INSERT or UPDATE happens, we
ought to also check that the NEW row matches the RLS qual. I don't
find that to be terribly important because you can accomplish the same
thing with a per-row trigger today; and I also don't think everyone
will want that behavior. Some people will, I'm pretty sure, want to
let users "give away" rows, either unconditionally or subject to
defined restrictions. Perhaps it's worth having, but it's a separate
feature, IMHO.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
2013/9/1 Greg Stark <stark@mit.edu>:
On Sun, Sep 1, 2013 at 8:31 PM, Kohei KaiGai <kaigai@kaigai.gr.jp> wrote:
Or, any other criteria even though?
My (current) preference is plan (c: we will be able to fix up *this*
cover-channel with reasonable efforts on explain code. probably,
we can close it if we don't print filtered rows under the sub-query
with security-barrier attribute.I think the criteria being discussed in this thread are too strict.
It may be the case that Postgres cannot make a strong general case
that it protects against covert channels. However it may still be able
to make the much weaker case that it is *possible* to arrange your
database such that the covert channels are kept under control.
Yes. I have to admit it is difficult to determine a strict and regular rule
to handle covert-channel scenario.
Sorry, the later half of this sentence is uncertain for me.
Are you saying, even if we could have a strict rule, we may have many
possible covert channel for information leakage??
So I think up above Tom is wrong about why it's ok that INSERT leaks
keys when it reports a unique key violation. The reason why it's ok
that there's a covert channel there is that the DBA can avoid that
covert channel by being careful when creating unique constraints. He
or she should be aware that creating a unique constraint implicitly
provides a kind of limited access to data to users who have INSERT
privilege even if they lack the real SELECT privilege.
IIRC, we discussed and concluded that the above information leakage
scenario shall be described in the documentation, and the way to
avoid valuable information leakage using alternative keys, a few years
before.
Likewise, as long as the covert channels in RLS are things the DBA has
even a modicum of control over I wouldn't be too worried. Afaict from
skimming the thread it looks like creating any indexes on columns
leaks what values of the index key exist in the table. Is it the case
that non-indexed columns do not get leaked?
According to the scenario reported by Korotkov, he could find number
of rows being filtered by the given qualifier, thus it implies existence of
the row with a value in a particular range.
Its solution is that I noted above, and I'm working for it now.
Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
2013/9/3 Bruce Momjian <bruce@momjian.us>:
On Sun, Sep 1, 2013 at 11:05:58AM -0700, Josh Berkus wrote:
Security community also concludes it is not avoidable nature as long
as human can observe system behavior and estimate something, thus,
security evaluation criteria does not require eliminate covert-channels
or does not pay attention about covert-channels for the products that
is installed on the environment with basic robustness (that means,
non-military, regular enterprise usage).To be completely blunt, the security community does not understand
databases. At all. I'd think if anything had become clear through the
course of work on SEPosgres, it would be that.Agreed. The security community realizes these covert channels exist,
but doesn't really have any recommendations on how to avoid them. You
could argue that avoiding them is too tied to specific database
implementations, but there are general channels, like insert with a
unique key, that should at least have well-defined solutions.
The security community also provides an extreme solution, but I don't
think it is suitable for flexible security policy and PostgreSQL wants it.
Their "extreme" solution manipulate definition of PK that automatically
become combined key that takes user-given key and security level being
set mandatory.
Thus, it does not conflict even if two different users with different security
level tries to insert a row with same primary key.
This technology is called polyinstantiation.
http://en.wikipedia.org/wiki/Polyinstantiation
However, of course, I'm not favor to port this technology to PostgreSQL
world. Its side-effects are too much towards the problem to be solved.
Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Robert Haas <robertmhaas@gmail.com> writes:
On Wed, Sep 4, 2013 at 10:50 AM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
Robert Haas <robertmhaas@gmail.com> writes:
Right. IMHO, this new feature should be similarly simple: when an
unprivileged user references a table, treat that as a reference to a
leakproof view over the table, with the RLS qual injected into the
view.
And for insert/update/delete, we do what exactly?
The same mechanism will prevent UPDATE and DELETE from seeing any rows
the user shouldn't be able to touch.
No, it won't, because we don't support direct update/delete on views
(and if you look, you'll notice the auto-updatable-view stuff doesn't
think a security-barrier view is auto-updatable).
AFAICT, to deal with update/delete the RLS patch needs to constrain order
of qual application without the crutch of having a separate level of
subquery; and it's that behavior that I have zero confidence in, either
as to whether it works as submitted or as to our odds of not breaking it
in the future.
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Robert Haas <robertmhaas@gmail.com> wrote:
Tom Lane <tgl@sss.pgh.pa.us> wrote:
Robert Haas <robertmhaas@gmail.com> writes:
IMHO, this new feature should be similarly simple: when an
unprivileged user references a table, treat that as a reference
to a leakproof view over the table, with the RLS qual injected
into the view.And for insert/update/delete, we do what exactly?
The same mechanism will prevent UPDATE and DELETE from seeing any
rows the user shouldn't be able to touch.
+1
Simon and Greg are arguing that when an INSERT or UPDATE happens,
we ought to also check that the NEW row matches the RLS qual. I
don't find that to be terribly important because you can
accomplish the same thing with a per-row trigger today; and I
also don't think everyone will want that behavior.
As an example from my Wisconsin Courts days, source documents come
in which need to be entered, which may contain a Social Security
Number, and most of the Clerk of Courts staff should be authorized
to enter that into the database. Once it is entered, most people
should not be allowed to view it, including many of the people who
were authorized to enter it initially. It's one thing for a line
staff person to have a handful of documents come across their desk
with SSN on a given day; it's quite another if they could dump a
list of names, addresses, dates of birth, and SSNs for the entire
database on demand.
In a way this issue is similar to the covert channel issue --
volume matters.
--
Kevin Grittner
EDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
2013/9/4 Tom Lane <tgl@sss.pgh.pa.us>:
Robert Haas <robertmhaas@gmail.com> writes:
On Wed, Sep 4, 2013 at 10:45 AM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
Well, the security-barrier view stuff did not present itself as a 100%
solution. But perhaps more to the point, it was conceptually simple to
implement, ie don't flatten views if they have this bit set, and don't
push down quals into such sub-selects unless they're marked leakproof.Right. IMHO, this new feature should be similarly simple: when an
unprivileged user references a table, treat that as a reference to a
leakproof view over the table, with the RLS qual injected into the
view.And for insert/update/delete, we do what exactly?
This patch does not care about insert, because it shall be done around the
place where we usually put before-row-insert; that is not related to planner.
Regarding to update/delete, this patch also enhanced to allow update or
delete mechanism allows to take a sub-query on top of the table scan plan.
So, its explain output shows as follows:
postgres=> EXPLAIN (costs off) UPDATE customer SET email = 'alice@example.com';
QUERY PLAN
--------------------------------------------------
Update on customer
-> Subquery Scan on customer
-> Seq Scan on customer customer_1
Filter: ("current_user"() = uname)
You can see this update has Subquery plan instead of regular relation scan.
Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
2013/9/4 Tom Lane <tgl@sss.pgh.pa.us>:
Robert Haas <robertmhaas@gmail.com> writes:
On Wed, Sep 4, 2013 at 10:50 AM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
Robert Haas <robertmhaas@gmail.com> writes:
Right. IMHO, this new feature should be similarly simple: when an
unprivileged user references a table, treat that as a reference to a
leakproof view over the table, with the RLS qual injected into the
view.And for insert/update/delete, we do what exactly?
The same mechanism will prevent UPDATE and DELETE from seeing any rows
the user shouldn't be able to touch.No, it won't, because we don't support direct update/delete on views
(and if you look, you'll notice the auto-updatable-view stuff doesn't
think a security-barrier view is auto-updatable).AFAICT, to deal with update/delete the RLS patch needs to constrain order
of qual application without the crutch of having a separate level of
subquery; and it's that behavior that I have zero confidence in, either
as to whether it works as submitted or as to our odds of not breaking it
in the future.
Are you suggesting to rewrite update / delete statement to filter out
unprivileged rows from manipulation?
Yes. I also thought it is a simple solution that does not need additional
enhancement to allow update / delete to take sub-query on top of reader
side plan.
For example, if security policy is (t1.owner = current_user) and the given
query was "UPDATE t1 SET value = value || '_updated' WHERE value like '%abc%'",
this query may be able to rewritten as follows:
UPDATE t1 SET value = value || '_updated' WHERE tid = (
SELECT tid FROM t1 WHERE t1.owner = current_user
) AND value like '%abc%';
This approach makes implementation simple, but it has to scan the
relation twice, thus its performance it not ideal, according to the
past discussion.
Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
2013/9/4 Kevin Grittner <kgrittn@ymail.com>:
Robert Haas <robertmhaas@gmail.com> wrote:
Tom Lane <tgl@sss.pgh.pa.us> wrote:
Robert Haas <robertmhaas@gmail.com> writes:
IMHO, this new feature should be similarly simple: when an
unprivileged user references a table, treat that as a reference
to a leakproof view over the table, with the RLS qual injected
into the view.And for insert/update/delete, we do what exactly?
The same mechanism will prevent UPDATE and DELETE from seeing any
rows the user shouldn't be able to touch.+1
Simon and Greg are arguing that when an INSERT or UPDATE happens,
we ought to also check that the NEW row matches the RLS qual. I
don't find that to be terribly important because you can
accomplish the same thing with a per-row trigger today; and I
also don't think everyone will want that behavior.As an example from my Wisconsin Courts days, source documents come
in which need to be entered, which may contain a Social Security
Number, and most of the Clerk of Courts staff should be authorized
to enter that into the database. Once it is entered, most people
should not be allowed to view it, including many of the people who
were authorized to enter it initially. It's one thing for a line
staff person to have a handful of documents come across their desk
with SSN on a given day; it's quite another if they could dump a
list of names, addresses, dates of birth, and SSNs for the entire
database on demand.In a way this issue is similar to the covert channel issue --
volume matters.
I think an important nature of this behavior is it is configurable.
In case when both of reader and writer side need to have same
security policy, it's good. One configuration allows to apply
a consistent security policy to fetch a row from table, and to
write a row to table.
If they don't want to check security policy on writer side,
all they need to do is setting a security policy for SELECT
only, even though its functionality is not implemented yet.
Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Kohei KaiGai <kaigai@kaigai.gr.jp> writes:
2013/9/4 Tom Lane <tgl@sss.pgh.pa.us>:
And for insert/update/delete, we do what exactly?
Regarding to update/delete, this patch also enhanced to allow update or
delete mechanism allows to take a sub-query on top of the table scan plan.
So, its explain output shows as follows:
postgres=> EXPLAIN (costs off) UPDATE customer SET email = 'alice@example.com';
QUERY PLAN
--------------------------------------------------
Update on customer
-> Subquery Scan on customer
-> Seq Scan on customer customer_1
Filter: ("current_user"() = uname)
You can see this update has Subquery plan instead of regular relation scan.
Really? That wasn't apparent from reading the patch. (Have I mentioned
it's desperately underdocumented? Aside from needing a lot more in-code
comments than it's got, it would benefit from having an overview section
added to optimizer/README explaining stuff at the level of this
discussion.)
I'm a bit surprised that setrefs.c doesn't eliminate the Subquery Scan as
being a no-op, given that no quals end up getting applied at that level.
You might look into why not, since if that plan node were eliminated at
the end, it'd fix any efficiency complaints about this approach.
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Wed, Sep 4, 2013 at 11:22 AM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
The same mechanism will prevent UPDATE and DELETE from seeing any rows
the user shouldn't be able to touch.No, it won't, because we don't support direct update/delete on views
(and if you look, you'll notice the auto-updatable-view stuff doesn't
think a security-barrier view is auto-updatable).AFAICT, to deal with update/delete the RLS patch needs to constrain order
of qual application without the crutch of having a separate level of
subquery; and it's that behavior that I have zero confidence in, either
as to whether it works as submitted or as to our odds of not breaking it
in the future.
I don't really see why. AIUI, the ModifyTable node just needs to get
the right TIDs. It's not like that has to be stacked directly on top
of a scan; indeed, in cases like UPDATE .. FROM and DELETE .. USING it
already isn't. Maybe there's some reason why the intervening level
can be a Join but not a SubqueryScan, but if so I'd expect we could
find some way of lifting that limitation without suffering too much
pain.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Wed, 2013-08-28 at 13:56 +0200, Kohei KaiGai wrote:
The attached patch fixed the portion I was pointed out, so its overall
design has not been changed so much.
The documentation doesn't build:
openjade:catalogs.sgml:237:28:X: reference to non-existent ID "CATALOG-PG-ROWLEVELSEC"
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Now I'm trying to tackle the covert-channel problem that Korotkov
pointed out at upthread.
The attached patch works "almost" well. It prevents to print number of
rows being filtered
if the target plan node is under sub-query with security-barrier
attribute; because row-
level security feature is constructed on existing security-barrier
view infrastructure.
One remaining issue is that planner pulls-up underlying relation scan
if top-level
sub-query is enough simple; probably, in case when targetlist of upper sub-query
is compatible with underlying relation scan.
See, the example below:
postgres=# CREATE TABLE t1 (a int primary key, b int);
CREATE TABLE
postgres=# INSERT INTO t1 VALUES (1,1111),(2,2222),(3,3333),(4,4444),(5,5555);
INSERT 0 5
postgres=# CREATE VIEW v1 WITH(security_barrier) AS SELECT * FROM t1
WHERE a % 2 = 1;
CREATE VIEW
postgres=# SET enable_seqscan = off;
SET
postgres=# EXPLAIN ANALYZE SELECT 1 FROM v1 WHERE b BETWEEN 2000 AND 4000;
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------
Subquery Scan on v1 (cost=10000000000.00..10000000052.81 rows=1
width=0) (actual time=0.016..0.017 rows=1 loops=1)
-> Seq Scan on t1 (cost=10000000000.00..10000000052.80 rows=1
width=8) (actual time=0.014..0.015 rows=1 loops=1)
Filter: ((b >= 2000) AND (b <= 4000) AND ((a % 2) = 1))
Total runtime: 0.067 ms
(4 rows)
According to the scenario that Korotkov pointed out, number of
filtered rows shall be
printed, thus, it allows to estimate particular value with log(N) times trials.
However, this example hides number of rows being done within
security-barrier sub-
query as I expected.
On the other hand, if planner pulled-up underlying relation scan, it
does NOT work fine.
postgres=# EXPLAIN ANALYZE SELECT * FROM v1 WHERE b BETWEEN 2000 AND 4000;
QUERY PLAN
----------------------------------------------------------------------------------------------------------------
Seq Scan on t1 (cost=10000000000.00..10000000052.80 rows=1 width=8)
(actual time=0.019..0.021 rows=1 loops=1)
Filter: ((b >= 2000) AND (b <= 4000) AND ((a % 2) = 1))
Rows Removed by Filter: 4
Total runtime: 0.055 ms
(4 rows)
It seems to me we need to prohibit to pull-up security-barrier
sub-query here, or
to mark underlying relation scan security-barrier attribute to solve this issue.
(I'm still looking for which routine handles this pull-up job,
however, I didn't find it yet.)
How about opinion for this solution?
Thanks,
2013/9/7 Peter Eisentraut <peter_e@gmx.net>:
On Wed, 2013-08-28 at 13:56 +0200, Kohei KaiGai wrote:
The attached patch fixed the portion I was pointed out, so its overall
design has not been changed so much.The documentation doesn't build:
openjade:catalogs.sgml:237:28:X: reference to non-existent ID "CATALOG-PG-ROWLEVELSEC"
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
Attachments:
explain_hide_filtered.v0.patchapplication/octet-stream; name=explain_hide_filtered.v0.patchDownload
src/backend/commands/explain.c | 107 +++++++++++++++++++++++++++--------------
1 file changed, 70 insertions(+), 37 deletions(-)
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 91bea51..4ea4920 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -57,7 +57,7 @@ static void ExplainPreScanMemberNodes(List *plans, PlanState **planstates,
static void ExplainPreScanSubPlans(List *plans, Bitmapset **rels_used);
static void ExplainNode(PlanState *planstate, List *ancestors,
const char *relationship, const char *plan_name,
- ExplainState *es);
+ bool security_barrier, ExplainState *es);
static void show_plan_tlist(PlanState *planstate, List *ancestors,
ExplainState *es);
static void show_expression(Node *node, const char *qlabel,
@@ -92,9 +92,12 @@ static void ExplainModifyTarget(ModifyTable *plan, ExplainState *es);
static void ExplainTargetRel(Plan *plan, Index rti, ExplainState *es);
static void show_modifytable_info(ModifyTableState *mtstate, ExplainState *es);
static void ExplainMemberNodes(List *plans, PlanState **planstates,
- List *ancestors, ExplainState *es);
+ List *ancestors, bool security_barrier, ExplainState *es);
static void ExplainSubPlans(List *plans, List *ancestors,
- const char *relationship, ExplainState *es);
+ const char *relationship, bool security_barrier,
+ ExplainState *es);
+static void ExplainSubQuery(SubqueryScanState *qstate, List *ancestors,
+ bool security_barrier, ExplainState *es);
static void ExplainProperty(const char *qlabel, const char *value,
bool numeric, ExplainState *es);
static void ExplainOpenGroup(const char *objtype, const char *labelname,
@@ -555,7 +558,7 @@ ExplainPrintPlan(ExplainState *es, QueryDesc *queryDesc)
es->rtable = queryDesc->plannedstmt->rtable;
ExplainPreScanNode(queryDesc->planstate, &rels_used);
es->rtable_names = select_rtable_names_for_explain(es->rtable, rels_used);
- ExplainNode(queryDesc->planstate, NIL, NULL, NULL, es);
+ ExplainNode(queryDesc->planstate, NIL, NULL, NULL, false, es);
}
/*
@@ -794,6 +797,10 @@ ExplainPreScanSubPlans(List *plans, Bitmapset **rels_used)
* (eg, "Outer", "Inner"); it can be null at top level. plan_name is an
* optional name to be attached to the node.
*
+ * security_barrier shows whether this plan come from a view with security
+ * barrier attribute, or not. If true, it restrain a part of output to
+ * avoid information leakage to be invisible.
+ *
* In text format, es->indent is controlled in this function since we only
* want it to change at plan-node boundaries. In non-text formats, es->indent
* corresponds to the nesting depth of logical output groups, and therefore
@@ -802,7 +809,7 @@ ExplainPreScanSubPlans(List *plans, Bitmapset **rels_used)
static void
ExplainNode(PlanState *planstate, List *ancestors,
const char *relationship, const char *plan_name,
- ExplainState *es)
+ bool security_barrier, ExplainState *es)
{
Plan *plan = planstate->plan;
const char *pname; /* node type name for text output */
@@ -1210,26 +1217,26 @@ ExplainNode(PlanState *planstate, List *ancestors,
case T_IndexScan:
show_scan_qual(((IndexScan *) plan)->indexqualorig,
"Index Cond", planstate, ancestors, es);
- if (((IndexScan *) plan)->indexqualorig)
+ if (((IndexScan *) plan)->indexqualorig && !security_barrier)
show_instrumentation_count("Rows Removed by Index Recheck", 2,
planstate, es);
show_scan_qual(((IndexScan *) plan)->indexorderbyorig,
"Order By", planstate, ancestors, es);
show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
- if (plan->qual)
+ if (plan->qual && !security_barrier)
show_instrumentation_count("Rows Removed by Filter", 1,
planstate, es);
break;
case T_IndexOnlyScan:
show_scan_qual(((IndexOnlyScan *) plan)->indexqual,
"Index Cond", planstate, ancestors, es);
- if (((IndexOnlyScan *) plan)->indexqual)
+ if (((IndexOnlyScan *) plan)->indexqual && !security_barrier)
show_instrumentation_count("Rows Removed by Index Recheck", 2,
planstate, es);
show_scan_qual(((IndexOnlyScan *) plan)->indexorderby,
"Order By", planstate, ancestors, es);
show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
- if (plan->qual)
+ if (plan->qual && !security_barrier)
show_instrumentation_count("Rows Removed by Filter", 1,
planstate, es);
if (es->analyze)
@@ -1243,7 +1250,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
case T_BitmapHeapScan:
show_scan_qual(((BitmapHeapScan *) plan)->bitmapqualorig,
"Recheck Cond", planstate, ancestors, es);
- if (((BitmapHeapScan *) plan)->bitmapqualorig)
+ if (((BitmapHeapScan *) plan)->bitmapqualorig && !security_barrier)
show_instrumentation_count("Rows Removed by Index Recheck", 2,
planstate, es);
/* FALL THRU */
@@ -1253,7 +1260,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
case T_WorkTableScan:
case T_SubqueryScan:
show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
- if (plan->qual)
+ if (plan->qual && !security_barrier)
show_instrumentation_count("Rows Removed by Filter", 1,
planstate, es);
break;
@@ -1263,7 +1270,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
"Function Call", planstate, ancestors,
es->verbose, es);
show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
- if (plan->qual)
+ if (plan->qual && !security_barrier)
show_instrumentation_count("Rows Removed by Filter", 1,
planstate, es);
break;
@@ -1279,14 +1286,14 @@ ExplainNode(PlanState *planstate, List *ancestors,
tidquals = list_make1(make_orclause(tidquals));
show_scan_qual(tidquals, "TID Cond", planstate, ancestors, es);
show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
- if (plan->qual)
+ if (plan->qual && !security_barrier)
show_instrumentation_count("Rows Removed by Filter", 1,
planstate, es);
}
break;
case T_ForeignScan:
show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
- if (plan->qual)
+ if (plan->qual && !security_barrier)
show_instrumentation_count("Rows Removed by Filter", 1,
planstate, es);
show_foreignscan_info((ForeignScanState *) planstate, es);
@@ -1294,11 +1301,11 @@ ExplainNode(PlanState *planstate, List *ancestors,
case T_NestLoop:
show_upper_qual(((NestLoop *) plan)->join.joinqual,
"Join Filter", planstate, ancestors, es);
- if (((NestLoop *) plan)->join.joinqual)
+ if (((NestLoop *) plan)->join.joinqual && !security_barrier)
show_instrumentation_count("Rows Removed by Join Filter", 1,
planstate, es);
show_upper_qual(plan->qual, "Filter", planstate, ancestors, es);
- if (plan->qual)
+ if (plan->qual && !security_barrier)
show_instrumentation_count("Rows Removed by Filter", 2,
planstate, es);
break;
@@ -1307,11 +1314,11 @@ ExplainNode(PlanState *planstate, List *ancestors,
"Merge Cond", planstate, ancestors, es);
show_upper_qual(((MergeJoin *) plan)->join.joinqual,
"Join Filter", planstate, ancestors, es);
- if (((MergeJoin *) plan)->join.joinqual)
+ if (((MergeJoin *) plan)->join.joinqual && !security_barrier)
show_instrumentation_count("Rows Removed by Join Filter", 1,
planstate, es);
show_upper_qual(plan->qual, "Filter", planstate, ancestors, es);
- if (plan->qual)
+ if (plan->qual && !security_barrier)
show_instrumentation_count("Rows Removed by Filter", 2,
planstate, es);
break;
@@ -1320,18 +1327,18 @@ ExplainNode(PlanState *planstate, List *ancestors,
"Hash Cond", planstate, ancestors, es);
show_upper_qual(((HashJoin *) plan)->join.joinqual,
"Join Filter", planstate, ancestors, es);
- if (((HashJoin *) plan)->join.joinqual)
+ if (((HashJoin *) plan)->join.joinqual && !security_barrier)
show_instrumentation_count("Rows Removed by Join Filter", 1,
planstate, es);
show_upper_qual(plan->qual, "Filter", planstate, ancestors, es);
- if (plan->qual)
+ if (plan->qual && !security_barrier)
show_instrumentation_count("Rows Removed by Filter", 2,
planstate, es);
break;
case T_Agg:
case T_Group:
show_upper_qual(plan->qual, "Filter", planstate, ancestors, es);
- if (plan->qual)
+ if (plan->qual && !security_barrier)
show_instrumentation_count("Rows Removed by Filter", 1,
planstate, es);
break;
@@ -1347,7 +1354,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
show_upper_qual((List *) ((Result *) plan)->resconstantqual,
"One-Time Filter", planstate, ancestors, es);
show_upper_qual(plan->qual, "Filter", planstate, ancestors, es);
- if (plan->qual)
+ if (plan->qual && !security_barrier)
show_instrumentation_count("Rows Removed by Filter", 1,
planstate, es);
break;
@@ -1487,17 +1494,18 @@ ExplainNode(PlanState *planstate, List *ancestors,
/* initPlan-s */
if (planstate->initPlan)
- ExplainSubPlans(planstate->initPlan, ancestors, "InitPlan", es);
+ ExplainSubPlans(planstate->initPlan, ancestors, "InitPlan",
+ security_barrier, es);
/* lefttree */
if (outerPlanState(planstate))
ExplainNode(outerPlanState(planstate), ancestors,
- "Outer", NULL, es);
+ "Outer", NULL, security_barrier, es);
/* righttree */
if (innerPlanState(planstate))
ExplainNode(innerPlanState(planstate), ancestors,
- "Inner", NULL, es);
+ "Inner", NULL, security_barrier, es);
/* special child plans */
switch (nodeTag(plan))
@@ -1505,31 +1513,31 @@ ExplainNode(PlanState *planstate, List *ancestors,
case T_ModifyTable:
ExplainMemberNodes(((ModifyTable *) plan)->plans,
((ModifyTableState *) planstate)->mt_plans,
- ancestors, es);
+ ancestors, security_barrier, es);
break;
case T_Append:
ExplainMemberNodes(((Append *) plan)->appendplans,
((AppendState *) planstate)->appendplans,
- ancestors, es);
+ ancestors, security_barrier, es);
break;
case T_MergeAppend:
ExplainMemberNodes(((MergeAppend *) plan)->mergeplans,
((MergeAppendState *) planstate)->mergeplans,
- ancestors, es);
+ ancestors, security_barrier, es);
break;
case T_BitmapAnd:
ExplainMemberNodes(((BitmapAnd *) plan)->bitmapplans,
((BitmapAndState *) planstate)->bitmapplans,
- ancestors, es);
+ ancestors, security_barrier, es);
break;
case T_BitmapOr:
ExplainMemberNodes(((BitmapOr *) plan)->bitmapplans,
((BitmapOrState *) planstate)->bitmapplans,
- ancestors, es);
+ ancestors, security_barrier, es);
break;
case T_SubqueryScan:
- ExplainNode(((SubqueryScanState *) planstate)->subplan, ancestors,
- "Subquery", NULL, es);
+ ExplainSubQuery((SubqueryScanState *) planstate,
+ ancestors, security_barrier, es);
break;
default:
break;
@@ -1537,7 +1545,8 @@ ExplainNode(PlanState *planstate, List *ancestors,
/* subPlan-s */
if (planstate->subPlan)
- ExplainSubPlans(planstate->subPlan, ancestors, "SubPlan", es);
+ ExplainSubPlans(planstate->subPlan, ancestors, "SubPlan",
+ security_barrier, es);
/* end of child plans */
if (haschildren)
@@ -2090,14 +2099,14 @@ show_modifytable_info(ModifyTableState *mtstate, ExplainState *es)
*/
static void
ExplainMemberNodes(List *plans, PlanState **planstates,
- List *ancestors, ExplainState *es)
+ List *ancestors, bool security_barrier, ExplainState *es)
{
int nplans = list_length(plans);
int j;
for (j = 0; j < nplans; j++)
ExplainNode(planstates[j], ancestors,
- "Member", NULL, es);
+ "Member", NULL, security_barrier, es);
}
/*
@@ -2108,7 +2117,8 @@ ExplainMemberNodes(List *plans, PlanState **planstates,
*/
static void
ExplainSubPlans(List *plans, List *ancestors,
- const char *relationship, ExplainState *es)
+ const char *relationship, bool security_barrier,
+ ExplainState *es)
{
ListCell *lst;
@@ -2118,11 +2128,34 @@ ExplainSubPlans(List *plans, List *ancestors,
SubPlan *sp = (SubPlan *) sps->xprstate.expr;
ExplainNode(sps->planstate, ancestors,
- relationship, sp->plan_name, es);
+ relationship, sp->plan_name, security_barrier, es);
}
}
/*
+ * Explain a child plan node of a particular sub-query
+ */
+static void
+ExplainSubQuery(SubqueryScanState *qstate, List *ancestors,
+ bool security_barrier, ExplainState *es)
+{
+ bool security_barrier_child;
+
+ if (!security_barrier)
+ {
+ SubqueryScan *qscan = (SubqueryScan *)qstate->ss.ps.plan;
+ RangeTblEntry *rte = rt_fetch(qscan->scan.scanrelid, es->rtable);
+
+ security_barrier_child = rte->security_barrier;
+ }
+ else
+ security_barrier_child = true;
+
+ ExplainNode(qstate->subplan, ancestors, "Subquery", NULL,
+ security_barrier_child, es);
+}
+
+/*
* Explain a property, such as sort keys or targets, that takes the form of
* a list of unlabeled items. "data" is a list of C strings.
*/
On 09/04/2013 11:22 PM, Tom Lane wrote:
AFAICT, to deal with update/delete the RLS patch needs to constrain order
of qual application without the crutch of having a separate level of
subquery; and it's that behavior that I have zero confidence in, either
as to whether it works as submitted or as to our odds of not breaking it
in the future.
Wouldn't CASE do that job, albeit in a somewhat ugly manner, and also
protect against malicious RLS functions?
For any subclause in a WHERE clause "(a) OR (b)" you can instead write a
short-circuit OR version as:
CASE WHEN (a) THEN true ELSE (b) END
no?
So, given a row security condition like "(rowowner = current_user AND
rls_malicious_leak())" on table "test", this:
SELECT * FROM test WHERE client_leak_fn();
could be rewritten by row security as:
SELECT *
FROM test
WHERE
CASE WHEN
CASE WHEN is_superuser() THEN true
ELSE (rowowner = current_user AND rls_malicious_leak())
END
THEN
client_leak_fn()
END;
in other words: if the user is the superuser, don't evaluate the RLS
predicate, just assume they have the right to see the row. Otherwise
evaluate the RLS predicate to determine whether they can see the row. In
either case, if they have the right to see the row, run the original
WHERE clause.
My main concern is that it'd be relying on a guarantee that CASE is
always completely ordered, and that might not be ideal. It's also
hideously ugly, and future optimiser smarts could create unexpected issues.
Unless you propose the creaton of a new short-circuit left-to-right
order guaranteed OR operator, and think the planner/optimizer should be
taught special case rules to prevent it from doing pull-up / push-down
or pre-evaluating subclauses for it, I suspect the notion of using
security barrier views or subqueries is still going to be the sanest way
to do it.
--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Craig Ringer <craig@2ndquadrant.com> writes:
On 09/04/2013 11:22 PM, Tom Lane wrote:
AFAICT, to deal with update/delete the RLS patch needs to constrain order
of qual application without the crutch of having a separate level of
subquery; and it's that behavior that I have zero confidence in, either
as to whether it works as submitted or as to our odds of not breaking it
in the future.
Wouldn't CASE do that job, albeit in a somewhat ugly manner, and also
protect against malicious RLS functions?
You mean wrap all the query quals in a CASE? Sure, if you didn't mind
totally destroying any optimization possibilities. If you did that,
every table scan would become a seqscan and every join a nestloop.
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Mon, Nov 4, 2013 at 9:37 AM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
Craig Ringer <craig@2ndquadrant.com> writes:
On 09/04/2013 11:22 PM, Tom Lane wrote:
AFAICT, to deal with update/delete the RLS patch needs to constrain order
of qual application without the crutch of having a separate level of
subquery; and it's that behavior that I have zero confidence in, either
as to whether it works as submitted or as to our odds of not breaking it
in the future.Wouldn't CASE do that job, albeit in a somewhat ugly manner, and also
protect against malicious RLS functions?You mean wrap all the query quals in a CASE? Sure, if you didn't mind
totally destroying any optimization possibilities. If you did that,
every table scan would become a seqscan and every join a nestloop.
I'd still like to here what's wrong with what I said here:
/messages/by-id/CA+TgmoYr1PHw3X9vnVuWDcfXkzK2p_jhtWc0fV2Q58NEgcxyTA@mail.gmail.com
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 11/04/2013 11:17 PM, Robert Haas wrote:
I'd still like to here what's wrong with what I said here:
/messages/by-id/CA+TgmoYr1PHw3X9vnVuWDcfXkzK2p_jhtWc0fV2Q58NEgcxyTA@mail.gmail.com
For me, just my understanding. I'm still too new to the planner and
rewriter to grasp that properly as written.
I was responding to Tom's objection, and he makes a good point about
CASE and optimisation. We have to be free to re-order and pre-evaluate
where LEAKPROOF flags make it safe and permissible, without ever
otherwise doing so. That makes the SECURITY BARRIER subquery look
better, since the limited pull-up / push-down is already implemented there.
Robert, any suggesitons on how to approach what you suggest? I'm pretty
new to the planner's guts, but I know there've been some complaints
about the way the current RLS code fiddles with Vars when it changes a
direct scan of a rel into a subquery scan.
The code in:
https://github.com/ringerc/postgres/blob/rls-9.4/src/backend/optimizer/prep/prepunion.c#L1647
and
https://github.com/ringerc/postgres/blob/rls-9.4/src/backend/optimizer/prep/prepunion.c#L1591
seems to be the one folks were complaining about earlier.
--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Mon, Nov 4, 2013 at 8:46 PM, Craig Ringer <craig@2ndquadrant.com> wrote:
On 11/04/2013 11:17 PM, Robert Haas wrote:
I'd still like to here what's wrong with what I said here:
/messages/by-id/CA+TgmoYr1PHw3X9vnVuWDcfXkzK2p_jhtWc0fV2Q58NEgcxyTA@mail.gmail.com
For me, just my understanding. I'm still too new to the planner and
rewriter to grasp that properly as written.I was responding to Tom's objection, and he makes a good point about
CASE and optimisation. We have to be free to re-order and pre-evaluate
where LEAKPROOF flags make it safe and permissible, without ever
otherwise doing so. That makes the SECURITY BARRIER subquery look
better, since the limited pull-up / push-down is already implemented there.Robert, any suggesitons on how to approach what you suggest? I'm pretty
new to the planner's guts, but I know there've been some complaints
about the way the current RLS code fiddles with Vars when it changes a
direct scan of a rel into a subquery scan.The code in:
https://github.com/ringerc/postgres/blob/rls-9.4/src/backend/optimizer/prep/prepunion.c#L1647
and
https://github.com/ringerc/postgres/blob/rls-9.4/src/backend/optimizer/prep/prepunion.c#L1591
seems to be the one folks were complaining about earlier.
I haven't studied this patch in detail, but I see why there's some
unhappiness about that code: it's an RLS-specific kluge. Just
shooting from the hip here, maybe we should attack the problem of
making security-barrier views updatable first, as a separate patch. I
would think that if we make that work, this will also work without,
hopefully, any special hackery. And we'd get a separate,
independently useful feature out of it, too.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 11/05/2013 09:36 PM, Robert Haas wrote:
I haven't studied this patch in detail, but I see why there's some
unhappiness about that code: it's an RLS-specific kluge. Just
shooting from the hip here, maybe we should attack the problem of
making security-barrier views updatable first, as a separate patch.
That's the approach I've been considering. There are a few wrinkles with
it, though:
(a) Updatable views are implemented in the rewriter, not the planner.
The rewriter is not re-run when plans are invalidated or when the
session authorization changes, etc. This means that we can't simply omit
the RLS predicate for superuser because the same rewritten parse tree
might get used for both superuser and non-superuser queries.
Options:
* Store the before-rewrite parse tree when RLS is discovered on one of
the rels in the tree. Re-run the rewriter when re-planning. Ensure a
change in current user always invalidates plans.
* Declare that it's not legal to run a query originally parsed and
rewritten as superuser as a non-superuser or vice versa. This would
cause a great deal of pain with PL/PgSQL.
* Always add the RLS predicate and solve the problem of reliably
short-circuiting the user-supplied predicate for RLS-exempt users. We'd
need a way to allow direct (not query-based) COPY TO for tables with RLS
applied, preventing the rewriting of direct table access into subqueries
for COPY, but since we never save plans for COPY that may be fine.
* ... ?
(b) Inheritance is a problem when RLS is done in the rewriter. As I
understood it from Kohei KaiGai's description to me earlier, there was a
strong preference on -hackers to enforce RLS predicates for child and
parent tables completely independently. That's how RLS currently works,
but it might be hard to get the same effect when applying RLS in the
rewriter. We'd need to solve that, or redefine RLS's behaviour so that
the predicate on a parent table applies to any child tables too.
Personally I'd prefer the latter.
(c) RLS might interact differently with rules declared on tables if
implemented in the rewriter, so some investigation into that would be
needed.
I
would think that if we make that work, this will also work without,
hopefully, any special hackery. And we'd get a separate,
independently useful feature out of it, too.
I tend to agree. I'm just a bit concerned about dealing with the issues
around RLS-exempt operations and users.
--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 6 November 2013 06:38, Craig Ringer <craig@2ndquadrant.com> wrote:
On 11/05/2013 09:36 PM, Robert Haas wrote:
I haven't studied this patch in detail, but I see why there's some
unhappiness about that code: it's an RLS-specific kluge. Just
shooting from the hip here, maybe we should attack the problem of
making security-barrier views updatable first, as a separate patch.That's the approach I've been considering.
FWIW, I have a half-complete patch to make SB views auto-updatable,
borrowing some code from the RLS patch. I hadn't looked at it for some
time, but I've just brushed the dust off it and rebased it to head.
There are several things I don't like in it, and it's probably not
close to being committable, but it might give you some ideas if you
were going to take a crack at this yourself.
The basic idea is to have rewriteTargetView() collect up any quals
from SB views in a new list on the target RTE, instead of adding them
to the main query's predicates (it needs to be a list of SB quals, in
case there are SB views on top of other SB views, in which case they
need to be kept separate from one another). Then at the end of the
rewriting process (after any views referenced in the SB quals have
been expanded), a new piece of code kicks in to process any RTEs with
SB quals, turning them into (possibly nested) subquery RTEs.
The complication is that the query's resultRelation RTE mustn't be a
subquery. This is handled this in a similar way to the
trigger-updatable views code, producing 2 RTEs --- the resultRelation
RTE becomes a direct reference to the base relation, and a separate
subquery RTE acts as the source of rows to update.
An additional complication arising from that approach is that the
planner's preptlist code (expand_targetlist) may add additional
targetlist entries representing identity assignments, so it needs to
know that the source RTE is now different from the result RTE in the
query, otherwise it
all falls apart because the target RTE isn't in the query's fromList.
That's one of the things I don't like about this. In fact it seems
messy that there are 2 separate places in the code that expand the
targetlist.
Anyway, feel free to do what you like with this. I wasn't planning on
submitting it to the next commitfest myself, because my non-PG
workload is too high, and I don't expect to get much time to hack on
postgresql during the next couple of months.
Regards,
Dean
Attachments:
updatable-sb-views.patchtext/x-patch; charset=US-ASCII; name=updatable-sb-views.patchDownload
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
new file mode 100644
index 246a3a9..bfc4fef
*** a/contrib/postgres_fdw/postgres_fdw.c
--- b/contrib/postgres_fdw/postgres_fdw.c
*************** postgresAddForeignUpdateTargets(Query *p
*** 1093,1099 ****
*/
/* Make a Var representing the desired value */
! var = makeVar(parsetree->resultRelation,
SelfItemPointerAttributeNumber,
TIDOID,
-1,
--- 1093,1099 ----
*/
/* Make a Var representing the desired value */
! var = makeVar(parsetree->sourceRelation,
SelfItemPointerAttributeNumber,
TIDOID,
-1,
diff --git a/doc/src/sgml/ref/create_view.sgml b/doc/src/sgml/ref/create_view.sgml
new file mode 100644
index e0fbe1e..888410f
*** a/doc/src/sgml/ref/create_view.sgml
--- b/doc/src/sgml/ref/create_view.sgml
*************** CREATE VIEW vista AS SELECT text 'Hello
*** 323,334 ****
or set-returning functions.
</para>
</listitem>
-
- <listitem>
- <para>
- The view must not have the <literal>security_barrier</> property.
- </para>
- </listitem>
</itemizedlist>
</para>
--- 323,328 ----
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
new file mode 100644
index 65f3b98..ffe0e20
*** a/src/backend/nodes/copyfuncs.c
--- b/src/backend/nodes/copyfuncs.c
*************** _copyRangeTblEntry(const RangeTblEntry *
*** 2003,2008 ****
--- 2003,2009 ----
COPY_SCALAR_FIELD(checkAsUser);
COPY_BITMAPSET_FIELD(selectedCols);
COPY_BITMAPSET_FIELD(modifiedCols);
+ COPY_NODE_FIELD(securityQuals);
return newnode;
}
*************** _copyQuery(const Query *from)
*** 2451,2456 ****
--- 2452,2458 ----
COPY_SCALAR_FIELD(queryId);
COPY_SCALAR_FIELD(canSetTag);
COPY_NODE_FIELD(utilityStmt);
+ COPY_SCALAR_FIELD(sourceRelation);
COPY_SCALAR_FIELD(resultRelation);
COPY_SCALAR_FIELD(hasAggs);
COPY_SCALAR_FIELD(hasWindowFuncs);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
new file mode 100644
index 4c9b05e..27331d5
*** a/src/backend/nodes/equalfuncs.c
--- b/src/backend/nodes/equalfuncs.c
*************** _equalQuery(const Query *a, const Query
*** 846,851 ****
--- 846,852 ----
/* we intentionally ignore queryId, since it might not be set */
COMPARE_SCALAR_FIELD(canSetTag);
COMPARE_NODE_FIELD(utilityStmt);
+ COMPARE_SCALAR_FIELD(sourceRelation);
COMPARE_SCALAR_FIELD(resultRelation);
COMPARE_SCALAR_FIELD(hasAggs);
COMPARE_SCALAR_FIELD(hasWindowFuncs);
*************** _equalRangeTblEntry(const RangeTblEntry
*** 2258,2263 ****
--- 2259,2265 ----
COMPARE_SCALAR_FIELD(checkAsUser);
COMPARE_BITMAPSET_FIELD(selectedCols);
COMPARE_BITMAPSET_FIELD(modifiedCols);
+ COMPARE_NODE_FIELD(securityQuals);
return true;
}
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
new file mode 100644
index 908f397..38cced1
*** a/src/backend/nodes/nodeFuncs.c
--- b/src/backend/nodes/nodeFuncs.c
*************** range_table_walker(List *rtable,
*** 2008,2013 ****
--- 2008,2016 ----
return true;
break;
}
+
+ if (walker(rte->securityQuals, context))
+ return true;
}
return false;
}
*************** range_table_mutator(List *rtable,
*** 2731,2736 ****
--- 2734,2740 ----
MUTATE(newrte->values_lists, rte->values_lists, List *);
break;
}
+ MUTATE(newrte->securityQuals, rte->securityQuals, List *);
newrt = lappend(newrt, newrte);
}
return newrt;
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
new file mode 100644
index 817b149..092e7b9
*** a/src/backend/nodes/outfuncs.c
--- b/src/backend/nodes/outfuncs.c
*************** _outQuery(StringInfo str, const Query *n
*** 2239,2244 ****
--- 2239,2245 ----
else
appendStringInfoString(str, " :utilityStmt <>");
+ WRITE_INT_FIELD(sourceRelation);
WRITE_INT_FIELD(resultRelation);
WRITE_BOOL_FIELD(hasAggs);
WRITE_BOOL_FIELD(hasWindowFuncs);
*************** _outRangeTblEntry(StringInfo str, const
*** 2411,2416 ****
--- 2412,2418 ----
WRITE_OID_FIELD(checkAsUser);
WRITE_BITMAPSET_FIELD(selectedCols);
WRITE_BITMAPSET_FIELD(modifiedCols);
+ WRITE_NODE_FIELD(securityQuals);
}
static void
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
new file mode 100644
index d325bb3..719d6ba
*** a/src/backend/nodes/readfuncs.c
--- b/src/backend/nodes/readfuncs.c
*************** _readQuery(void)
*** 198,203 ****
--- 198,204 ----
local_node->queryId = 0; /* not saved in output format */
READ_BOOL_FIELD(canSetTag);
READ_NODE_FIELD(utilityStmt);
+ READ_INT_FIELD(sourceRelation);
READ_INT_FIELD(resultRelation);
READ_BOOL_FIELD(hasAggs);
READ_BOOL_FIELD(hasWindowFuncs);
*************** _readRangeTblEntry(void)
*** 1251,1256 ****
--- 1252,1258 ----
READ_OID_FIELD(checkAsUser);
READ_BITMAPSET_FIELD(selectedCols);
READ_BITMAPSET_FIELD(modifiedCols);
+ READ_NODE_FIELD(securityQuals);
READ_DONE();
}
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
new file mode 100644
index d8aa35d..19031bf
*** a/src/backend/optimizer/plan/planner.c
--- b/src/backend/optimizer/plan/planner.c
*************** inheritance_planner(PlannerInfo *root)
*** 891,896 ****
--- 891,898 ----
rte = copyObject(rte);
subroot.parse->rtable = lappend(subroot.parse->rtable,
rte);
+ if (subroot.parse->sourceRelation == rti)
+ subroot.parse->sourceRelation = newrti;
}
rti++;
}
diff --git a/src/backend/optimizer/prep/preptlist.c b/src/backend/optimizer/prep/preptlist.c
new file mode 100644
index fb67f9e..75a6c5d
*** a/src/backend/optimizer/prep/preptlist.c
--- b/src/backend/optimizer/prep/preptlist.c
***************
*** 36,45 ****
#include "utils/rel.h"
! static List *expand_targetlist(List *tlist, int command_type,
! Index result_relation, List *range_table);
/*
* preprocess_targetlist
* Driver for preprocessing the parse tree targetlist.
--- 36,74 ----
#include "utils/rel.h"
! static List *expand_targetlist(Query *parse, List *tlist, int command_type,
! Index source_relation, Index result_relation,
! List *range_table);
+ static AttrNumber
+ lookup_varattno(Query *parse, AttrNumber attno, Index rt_index, List *rtables)
+ {
+ RangeTblEntry *rte = rt_fetch(rt_index, rtables);
+
+ if (rte->rtekind == RTE_SUBQUERY && rt_index == parse->sourceRelation)
+ {
+ 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, "attno %d not found in subquery targetlist", attno);
+ }
+ return attno;
+ }
+
/*
* preprocess_targetlist
* Driver for preprocessing the parse tree targetlist.
*************** preprocess_targetlist(PlannerInfo *root,
*** 73,80 ****
* 10/94
*/
if (command_type == CMD_INSERT || command_type == CMD_UPDATE)
! tlist = expand_targetlist(tlist, command_type,
result_relation, range_table);
/*
* Add necessary junk columns for rowmarked rels. These values are needed
--- 102,113 ----
* 10/94
*/
if (command_type == CMD_INSERT || command_type == CMD_UPDATE)
! {
! Index source_relation = parse->sourceRelation > 0 ?
! parse->sourceRelation : result_relation;
! tlist = expand_targetlist(parse, tlist, command_type, source_relation,
result_relation, range_table);
+ }
/*
* Add necessary junk columns for rowmarked rels. These values are needed
*************** preprocess_targetlist(PlannerInfo *root,
*** 96,102 ****
{
/* It's a regular table, so fetch its TID */
var = makeVar(rc->rti,
! SelfItemPointerAttributeNumber,
TIDOID,
-1,
InvalidOid,
--- 129,136 ----
{
/* It's a regular table, so fetch its TID */
var = makeVar(rc->rti,
! lookup_varattno(parse, SelfItemPointerAttributeNumber,
! rc->rti, range_table),
TIDOID,
-1,
InvalidOid,
*************** preprocess_targetlist(PlannerInfo *root,
*** 112,118 ****
if (rc->isParent)
{
var = makeVar(rc->rti,
! TableOidAttributeNumber,
OIDOID,
-1,
InvalidOid,
--- 146,153 ----
if (rc->isParent)
{
var = makeVar(rc->rti,
! lookup_varattno(parse, TableOidAttributeNumber,
! rc->rti, range_table),
OIDOID,
-1,
InvalidOid,
*************** preprocess_targetlist(PlannerInfo *root,
*** 194,201 ****
* non-junk attributes appear in proper field order.
*/
static List *
! expand_targetlist(List *tlist, int command_type,
! Index result_relation, List *range_table)
{
List *new_tlist = NIL;
ListCell *tlist_item;
--- 229,237 ----
* non-junk attributes appear in proper field order.
*/
static List *
! expand_targetlist(Query *parse, List *tlist, int command_type,
! Index source_relation, Index result_relation,
! List *range_table)
{
List *new_tlist = NIL;
ListCell *tlist_item;
*************** expand_targetlist(List *tlist, int comma
*** 298,305 ****
case CMD_UPDATE:
if (!att_tup->attisdropped)
{
! new_expr = (Node *) makeVar(result_relation,
! attrno,
atttype,
atttypmod,
attcollation,
--- 334,344 ----
case CMD_UPDATE:
if (!att_tup->attisdropped)
{
! new_expr = (Node *) makeVar(source_relation,
! lookup_varattno(parse,
! attrno,
! source_relation,
! range_table),
atttype,
atttypmod,
attcollation,
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
new file mode 100644
index c52a374..660d62f
*** a/src/backend/rewrite/rewriteHandler.c
--- b/src/backend/rewrite/rewriteHandler.c
***************
*** 14,19 ****
--- 14,20 ----
#include "postgres.h"
#include "access/sysattr.h"
+ #include "catalog/heap.h"
#include "catalog/pg_type.h"
#include "commands/trigger.h"
#include "foreign/fdwapi.h"
*************** static Query *fireRIRrules(Query *parset
*** 63,68 ****
--- 64,70 ----
bool forUpdatePushedDown);
static bool view_has_instead_trigger(Relation view, CmdType event);
static Bitmapset *adjust_view_column_set(Bitmapset *cols, List *targetlist);
+ static Query *rewriteSecurityQuals(Query *parsetree);
/*
*************** view_query_is_auto_updatable(Query *view
*** 2048,2061 ****
return gettext_noop("Views that return set-returning functions are not automatically updatable.");
/*
- * For now, we also don't support security-barrier views, because of the
- * difficulty of keeping upper-level qual expressions away from
- * lower-level data. This might get relaxed in the future.
- */
- if (security_barrier)
- return gettext_noop("Security-barrier views are not automatically updatable.");
-
- /*
* The view query should select from a single base relation, which must be
* a table or another view.
*/
--- 2050,2055 ----
*************** rewriteTargetView(Query *parsetree, Rela
*** 2664,2669 ****
--- 2658,2671 ----
view_targetlist);
/*
+ * Move any security barrier quals from the view RTE onto the new target
+ * RTE. Any such quals should now apply to the new target RTE and will not
+ * reference the original view RTE in the rewritten query.
+ */
+ new_rte->securityQuals = view_rte->securityQuals;
+ view_rte->securityQuals = NIL;
+
+ /*
* For UPDATE/DELETE, rewriteTargetListUD will have added a wholerow junk
* TLE for the view to the end of the targetlist, which we no longer need.
* Remove it to avoid unnecessary work when we process the targetlist.
*************** rewriteTargetView(Query *parsetree, Rela
*** 2743,2748 ****
--- 2745,2754 ----
* only adjust their varnos to reference the new target (just the same as
* we did with the view targetlist).
*
+ * Note that there is special-case handling for the quals of a security
+ * barrier view, since they need to be kept separate from any user-supplied
+ * quals - see rewriteSecurityQuals.
+ *
* For INSERT, the view's quals can be ignored in the main query.
*/
if (parsetree->commandType != CMD_INSERT &&
*************** rewriteTargetView(Query *parsetree, Rela
*** 2751,2757 ****
Node *viewqual = (Node *) copyObject(viewquery->jointree->quals);
ChangeVarNodes(viewqual, base_rt_index, new_rt_index, 0);
! AddQual(parsetree, (Node *) viewqual);
}
/*
--- 2757,2774 ----
Node *viewqual = (Node *) copyObject(viewquery->jointree->quals);
ChangeVarNodes(viewqual, base_rt_index, new_rt_index, 0);
!
! if (RelationIsSecurityView(view))
! {
! /*
! * Note: the parsetree has been mutated, so the new_rte pointer is
! * stale and needs to be re-computed.
! */
! new_rte = rt_fetch(new_rt_index, parsetree->rtable);
! new_rte->securityQuals = lcons(viewqual, new_rte->securityQuals);
! }
! else
! AddQual(parsetree, (Node *) viewqual);
}
/*
*************** rewriteTargetView(Query *parsetree, Rela
*** 2812,2824 ****
/*
* Make sure that the query is marked correctly if the added
* qual has sublinks. We can skip this check if the query is
! * already marked, or if the command is an UPDATE, in which
! * case the same qual will have already been added to the
! * query's WHERE clause, and AddQual will have already done
! * this check.
*/
if (!parsetree->hasSubLinks &&
! parsetree->commandType != CMD_UPDATE)
parsetree->hasSubLinks = checkExprHasSubLink(wco->qual);
}
}
--- 2829,2842 ----
/*
* Make sure that the query is marked correctly if the added
* qual has sublinks. We can skip this check if the query is
! * already marked, or if the command is an UPDATE to a
! * non-security-barrier view, in which case the same qual will
! * have already been added to the query's WHERE clause, and
! * AddQual will have already done this check.
*/
if (!parsetree->hasSubLinks &&
! (parsetree->commandType != CMD_UPDATE ||
! RelationIsSecurityView(view)))
parsetree->hasSubLinks = checkExprHasSubLink(wco->qual);
}
}
*************** rewriteTargetView(Query *parsetree, Rela
*** 2829,2834 ****
--- 2847,3291 ----
/*
+ * rewriteSecurityQual -
+ * rewrite the specified security barrier qual on a query RTE, turning the
+ * RTE into a subquery.
+ */
+ static Query *
+ rewriteSecurityQual(Query *parsetree, int rt_index,
+ bool is_result_relation, Node *qual)
+ {
+ RangeTblEntry *rte;
+ List *targetlist = NIL;
+ List *rte_targetlist = NIL;
+ List *colnames = NIL;
+ Relation relation;
+ AttrNumber attno;
+ Var *var;
+ TargetEntry *tle;
+ Query *subquery;
+ RangeTblEntry *subrte;
+ RangeTblRef *subrtr;
+ ListCell *cell;
+
+ rte = rt_fetch(rt_index, parsetree->rtable);
+
+ /*
+ * There are 2 possible cases:
+ *
+ * 1. A relation RTE, which we turn into a subquery RTE containing all
+ * referenced columns.
+ *
+ * 2. A subquery RTE (either from a prior call to this function or from an
+ * expanded view). In this case we build a new subquery on top of it to
+ * isolate this security barrier qual from any other quals.
+ */
+ switch (rte->rtekind)
+ {
+ case RTE_RELATION:
+ /*
+ * Build the subquery targetlist from the non-dropped columns of
+ * the underlying table and an RTE targetlist to map varattnos in
+ * the main query onto the new subquery columns.
+ */
+ relation = heap_open(rte->relid, NoLock);
+
+ for (attno = 1; attno <= relation->rd_att->natts; attno++)
+ {
+ Form_pg_attribute att_tup;
+ char *attname;
+
+ /* Ignore dropped attributes */
+ att_tup = relation->rd_att->attrs[attno - 1];
+ if (att_tup->attisdropped)
+ continue;
+
+ /* target entry for new subquery targetlist */
+ var = makeVar(1,
+ attno,
+ att_tup->atttypid,
+ att_tup->atttypmod,
+ att_tup->attcollation,
+ 0);
+ attname = NameStr(att_tup->attname);
+
+ tle = makeTargetEntry((Expr *) var,
+ list_length(targetlist) + 1,
+ pstrdup(attname),
+ false);
+ targetlist = lappend(targetlist, tle);
+
+ /* top-level target entry for rewriting the main query */
+ var = copyObject(var);
+ var->varno = rt_index;
+ var->varattno = list_length(targetlist);
+ tle = makeTargetEntry((Expr *) var,
+ attno,
+ pstrdup(attname),
+ false);
+ rte_targetlist = lappend(rte_targetlist, tle);
+
+ colnames = lappend(colnames, makeString(pstrdup(attname)));
+ }
+
+ /*
+ * Then include any junk system attributes. These must come at
+ * the end of the targetlist.
+ */
+ for (attno = FirstLowInvalidHeapAttributeNumber + 1;
+ attno <= InvalidAttrNumber; attno++)
+ {
+ Form_pg_attribute att_tup;
+ char *attname;
+
+ if (attno == ObjectIdAttributeNumber &&
+ !relation->rd_rel->relhasoids)
+ continue;
+
+ if (attno == InvalidAttrNumber)
+ {
+ /* whole-row attribute */
+ var = makeWholeRowVar(rte, 1, 0, false);
+ attname = "wholerow";
+ }
+ else
+ {
+ /* system attribute */
+ att_tup = SystemAttributeDefinition(attno,
+ relation->rd_rel->relhasoids);
+
+ var = makeVar(1,
+ attno,
+ att_tup->atttypid,
+ att_tup->atttypmod,
+ att_tup->attcollation,
+ 0);
+ attname = NameStr(att_tup->attname);
+ }
+
+ /* target entry for new subquery targetlist */
+ tle = makeTargetEntry((Expr *) var,
+ list_length(targetlist) + 1,
+ pstrdup(attname),
+ false);
+ targetlist = lappend(targetlist, tle);
+
+ /* top-level target entry for rewriting the main query */
+ var = copyObject(var);
+ var->varno = rt_index;
+ var->varattno = list_length(targetlist);
+ tle = makeTargetEntry((Expr *) var,
+ attno,
+ pstrdup(attname),
+ false);
+ rte_targetlist = lappend(rte_targetlist, tle);
+
+ colnames = lappend(colnames, makeString(pstrdup(attname)));
+ }
+ heap_close(relation, NoLock);
+
+ /*
+ * Turn the main relation RTE into a security barrier subquery
+ * RTE, moving all permissions checks and rowMarks down into the
+ * subquery.
+ */
+ subquery = makeNode(Query);
+ subquery->commandType = CMD_SELECT;
+ subquery->querySource = QSRC_INSTEAD_RULE;
+ subquery->targetList = targetlist;
+
+ subrte = copyObject(rte);
+ subrte->inFromCl = true;
+ subrte->securityQuals = NIL;
+ subquery->rtable = list_make1(subrte);
+
+ subrtr = makeNode(RangeTblRef);
+ subrtr->rtindex = 1;
+ subquery->jointree = makeFromExpr(list_make1(subrtr), qual);
+ subquery->hasSubLinks = checkExprHasSubLink(qual);
+
+ foreach(cell, parsetree->rowMarks)
+ {
+ RowMarkClause *rc = (RowMarkClause *) lfirst(cell);
+ if (rc->rti == rt_index)
+ {
+ parsetree->rowMarks = list_delete(parsetree->rowMarks,
+ rc);
+ rc->rti = 1;
+ rc->pushedDown = true;
+ subquery->rowMarks = list_make1(rc);
+ break;
+ }
+ }
+
+ /*
+ * If this RTE was the result relation, then we need to lock the
+ * rows coming from it. Note that by the time we get here, this
+ * RTE will no longer be the result relation, so we have to rely
+ * on the flag passed in.
+ */
+ if (is_result_relation)
+ applyLockingClause(subquery, 1, LCS_FORUPDATE, false, true);
+
+ rte->rtekind = RTE_SUBQUERY;
+ rte->relid = InvalidOid;
+ rte->subquery = subquery;
+ rte->security_barrier = true;
+ rte->eref = makeAlias(rte->eref->aliasname, colnames);
+ rte->inh = false; /* must not be set for a subquery */
+
+ /* the permissions checks have now been moved down */
+ rte->requiredPerms = 0;
+ rte->checkAsUser = InvalidOid;
+ rte->selectedCols = NULL;
+ rte->modifiedCols = NULL;
+
+ /*
+ * Update any varattnos in the main query that refer to this RTE,
+ * using the entries from the RTE targetlist.
+ */
+ parsetree = (Query *)
+ ReplaceVarsFromTargetList((Node *) parsetree,
+ rt_index,
+ 0,
+ rte,
+ rte_targetlist,
+ REPLACEVARS_REPORT_ERROR,
+ 0,
+ &parsetree->hasSubLinks);
+ break;
+
+ case RTE_SUBQUERY:
+ /*
+ * Build a new subquery that includes all the same columns as the
+ * original subquery.
+ */
+ foreach(cell, rte->subquery->targetList)
+ {
+ tle = (TargetEntry *) lfirst(cell);
+ var = makeVarFromTargetEntry(1, tle);
+
+ tle = makeTargetEntry((Expr *) var,
+ list_length(targetlist) + 1,
+ pstrdup(tle->resname),
+ tle->resjunk);
+ targetlist = lappend(targetlist, tle);
+ }
+
+ subquery = makeNode(Query);
+ subquery->commandType = CMD_SELECT;
+ subquery->querySource = QSRC_INSTEAD_RULE;
+ subquery->targetList = targetlist;
+
+ subrte = makeNode(RangeTblEntry);
+ subrte->rtekind = RTE_SUBQUERY;
+ subrte->subquery = rte->subquery;
+ subrte->security_barrier = rte->security_barrier;
+ subrte->eref = copyObject(rte->eref);
+ subrte->inFromCl = true;
+ subquery->rtable = list_make1(subrte);
+
+ subrtr = makeNode(RangeTblRef);
+ subrtr->rtindex = 1;
+ subquery->jointree = makeFromExpr(list_make1(subrtr), qual);
+ subquery->hasSubLinks = checkExprHasSubLink(qual);
+
+ rte->subquery = subquery;
+ rte->security_barrier = true;
+
+ break;
+
+ default:
+ elog(ERROR, "invalid range table entry for security barrier qual");
+ }
+
+ return parsetree;
+ }
+
+
+ /*
+ * rewriteSecurityQualsOnSubLink -
+ * Apply rewriteSecurityQuals() to each SubLink (subselect in expression)
+ * found in the given tree.
+ *
+ * NOTE: although this has the form of a walker, we cheat and modify the
+ * SubLink nodes in-place. This is safe because we are not descending into
+ * subqueries, so no parts of the tree we are modifying are being traversed.
+ *
+ * Each SubLink subselect is replaced with a possibly-rewritten subquery.
+ */
+ static bool
+ rewriteSecurityQualsOnSubLink(Node *node, void *ctx)
+ {
+ if (node == NULL)
+ return false;
+ if (IsA(node, SubLink))
+ {
+ SubLink *sub = (SubLink *) node;
+
+ sub->subselect = (Node *)
+ rewriteSecurityQuals((Query *) sub->subselect);
+ /* Fall through to process lefthand args of SubLink */
+ }
+
+ /*
+ * Do NOT recurse into Query nodes, because rewriteSecurityQuals already
+ * processed all subqueries in the rtable and cteList.
+ */
+ return expression_tree_walker(node, rewriteSecurityQualsOnSubLink, ctx);
+ }
+
+
+ /*
+ * rewriteSecurityQuals -
+ * rewrites any security barrier quals on RTEs in the query, turning them
+ * into subqueries to allow the planner to enforce them before any user
+ * quals where necessary. Currently such security barrier quals can only
+ * have come from automatically updatable security barrier views.
+ *
+ * We do this at the end of the rewriting process (after any SELECT rules have
+ * been applied) so that the new security barrier subqueries wrap any remaining
+ * views after they are expanded.
+ *
+ * Any given RTE may have multiple security barrier quals in a list, from which
+ * we create a set of nested subqueries to isolate each security barrier from
+ * the others, providing protection against malicious user security barriers.
+ * The first item in the list represents the innermost subquery.
+ */
+ static Query *
+ rewriteSecurityQuals(Query *parsetree)
+ {
+ ListCell *l;
+ int rt_index;
+
+ /*
+ * Process each RTE in the rtable list. Security barrier quals are
+ * initially only added to the result relation, but subsequent rules
+ * may change that, so they may be anywhere.
+ *
+ * Note that this is deliberately not a foreach loop, since the whole
+ * parsetree may be mutated each time through the loop.
+ */
+ rt_index = 0;
+ while (rt_index < list_length(parsetree->rtable))
+ {
+ RangeTblEntry *rte;
+ bool is_result_relation;
+ int qual_idx;
+
+ ++rt_index;
+ rte = rt_fetch(rt_index, parsetree->rtable);
+
+ if (rte->securityQuals == NIL)
+ continue;
+
+ /*
+ * Ignore any RTEs that aren't used in the query (such RTEs may be
+ * present for permissions checks).
+ */
+ if (rt_index != parsetree->resultRelation &&
+ !rangeTableEntry_used((Node *) parsetree, rt_index, 0))
+ continue;
+
+ /*
+ * Recursively process any security barrier quals in subquery RTEs
+ * before processing any at this query level.
+ */
+ if (rte->rtekind == RTE_SUBQUERY)
+ rte->subquery = rewriteSecurityQuals(rte->subquery);
+
+ /*
+ * If this RTE is the target then we need to make a copy of it before
+ * expanding it. The unexpanded copy will become the new target, and
+ * the expanded RTE will be the source of rows to update/delete
+ * (cf. ApplyRetrieveRule).
+ */
+ is_result_relation = rt_index == parsetree->resultRelation;
+ if (is_result_relation)
+ {
+ RangeTblEntry *newrte = copyObject(rte);
+ parsetree->rtable = lappend(parsetree->rtable, newrte);
+ parsetree->sourceRelation = parsetree->resultRelation;
+ parsetree->resultRelation = list_length(parsetree->rtable);
+
+ /*
+ * Wipe out any copied security quals on the new target to prevent
+ * infinite recursion.
+ */
+ newrte->securityQuals = NIL;
+
+ /*
+ * There's no need to do permissions checks twice, so wipe out the
+ * permissions info for the original RTE (we prefer to keep the
+ * bits set on the result RTE).
+ */
+ rte->requiredPerms = 0;
+ rte->checkAsUser = InvalidOid;
+ rte->selectedCols = NULL;
+ rte->modifiedCols = NULL;
+
+ /*
+ * For the most part, Vars referencing the original relation
+ * should remain as as they are, meaning that they implicitly
+ * represent OLD values. But in the RETURNING list if any, we
+ * want such Vars to represent NEW values, so change them to
+ * reference the new RTE.
+ *
+ * Since ChangeVarNodes scribbles on the tree in-place, copy the
+ * RETURNING list first for safety.
+ */
+ parsetree->returningList = copyObject(parsetree->returningList);
+ ChangeVarNodes((Node *) parsetree->returningList, rt_index,
+ parsetree->resultRelation, 0);
+ }
+
+ /*
+ * Process each security qual in turn, starting with the innermost
+ * one and working outwards.
+ *
+ * Note that we can't use a foreach loop here because the whole
+ * parsetree may be mutated each time through the loop. For the same
+ * reason we must re-fetch the RTE each time.
+ */
+ qual_idx = 0;
+ while (qual_idx < list_length(rte->securityQuals))
+ {
+ Node *qual = (Node *) list_nth(rte->securityQuals, qual_idx);
+
+ ChangeVarNodes(qual, rt_index, 1, 0);
+ parsetree = rewriteSecurityQual(parsetree, rt_index,
+ is_result_relation, qual);
+
+ /* re-fetch the RTE in case it has been re-written */
+ rte = rt_fetch(rt_index, parsetree->rtable);
+
+ qual_idx++;
+ }
+ rte->securityQuals = NIL;
+ }
+
+ /* Recurse into subqueries in WITH */
+ foreach(l, parsetree->cteList)
+ {
+ CommonTableExpr *cte = (CommonTableExpr *) lfirst(l);
+
+ cte->ctequery = (Node *)
+ rewriteSecurityQuals((Query *) cte->ctequery);
+ }
+
+ /*
+ * Recurse into sublink subqueries too, without descending into the rtable
+ * or the cteList, which we have already processed.
+ */
+ if (parsetree->hasSubLinks)
+ query_tree_walker(parsetree, rewriteSecurityQualsOnSubLink, NULL,
+ QTW_IGNORE_RC_SUBQUERIES);
+
+ return parsetree;
+ }
+
+
+ /*
* RewriteQuery -
* rewrites the query and apply the rules again on the queries rewritten
*
*************** QueryRewrite(Query *parsetree)
*** 3218,3224 ****
/*
* Step 2
*
! * Apply all the RIR rules on each query
*
* This is also a handy place to mark each query with the original queryId
*/
--- 3675,3682 ----
/*
* Step 2
*
! * Apply all the RIR rules on each query, and then expand any security
! * quals that apply to RTEs in each query.
*
* This is also a handy place to mark each query with the original queryId
*/
*************** QueryRewrite(Query *parsetree)
*** 3228,3233 ****
--- 3686,3692 ----
Query *query = (Query *) lfirst(l);
query = fireRIRrules(query, NIL, false);
+ query = rewriteSecurityQuals(query);
query->queryId = input_query_id;
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
new file mode 100644
index e5235cb..5e89651
*** a/src/include/nodes/parsenodes.h
--- b/src/include/nodes/parsenodes.h
*************** typedef struct Query
*** 110,115 ****
--- 110,118 ----
Node *utilityStmt; /* non-null if this is DECLARE CURSOR or a
* non-optimizable statement */
+ int sourceRelation; /* rtable index of source relation for
+ * UPDATE/DELETE; 0 otherwise */
+
int resultRelation; /* rtable index of target relation for
* INSERT/UPDATE/DELETE; 0 for SELECT */
*************** typedef struct RangeTblEntry
*** 801,806 ****
--- 804,810 ----
Oid checkAsUser; /* if valid, check access as this role */
Bitmapset *selectedCols; /* columns needing SELECT permission */
Bitmapset *modifiedCols; /* columns needing INSERT/UPDATE permission */
+ List *securityQuals; /* any security barrier quals to apply */
} RangeTblEntry;
/*
diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out
new file mode 100644
index c725bba..b4d0af2
*** a/src/test/regress/expected/updatable_views.out
--- b/src/test/regress/expected/updatable_views.out
*************** CREATE VIEW rw_view14 AS SELECT ctid, a,
*** 22,33 ****
CREATE VIEW rw_view15 AS SELECT a, upper(b) FROM base_tbl; -- Expression/function may be part of an updatable view
CREATE VIEW rw_view16 AS SELECT a, b, a AS aa FROM base_tbl; -- Repeated column may be part of an updatable view
CREATE VIEW ro_view17 AS SELECT * FROM ro_view1; -- Base relation not updatable
! CREATE VIEW ro_view18 WITH (security_barrier = true)
! AS SELECT * FROM base_tbl; -- Security barrier views not updatable
! CREATE VIEW ro_view19 AS SELECT * FROM (VALUES(1)) AS tmp(a); -- VALUES in rangetable
CREATE SEQUENCE seq;
! CREATE VIEW ro_view20 AS SELECT * FROM seq; -- View based on a sequence
! CREATE VIEW ro_view21 AS SELECT a, b, generate_series(1, a) g FROM base_tbl; -- SRF in targetlist not supported
SELECT table_name, is_insertable_into
FROM information_schema.tables
WHERE table_name LIKE E'r_\\_view%'
--- 22,31 ----
CREATE VIEW rw_view15 AS SELECT a, upper(b) FROM base_tbl; -- Expression/function may be part of an updatable view
CREATE VIEW rw_view16 AS SELECT a, b, a AS aa FROM base_tbl; -- Repeated column may be part of an updatable view
CREATE VIEW ro_view17 AS SELECT * FROM ro_view1; -- Base relation not updatable
! CREATE VIEW ro_view18 AS SELECT * FROM (VALUES(1)) AS tmp(a); -- VALUES in rangetable
CREATE SEQUENCE seq;
! CREATE VIEW ro_view19 AS SELECT * FROM seq; -- View based on a sequence
! CREATE VIEW ro_view20 AS SELECT a, b, generate_series(1, a) g FROM base_tbl; -- SRF in targetlist not supported
SELECT table_name, is_insertable_into
FROM information_schema.tables
WHERE table_name LIKE E'r_\\_view%'
*************** SELECT table_name, is_insertable_into
*** 44,50 ****
ro_view19 | NO
ro_view2 | NO
ro_view20 | NO
- ro_view21 | NO
ro_view3 | NO
ro_view4 | NO
ro_view5 | NO
--- 42,47 ----
*************** SELECT table_name, is_insertable_into
*** 55,61 ****
rw_view14 | YES
rw_view15 | YES
rw_view16 | YES
! (21 rows)
SELECT table_name, is_updatable, is_insertable_into
FROM information_schema.views
--- 52,58 ----
rw_view14 | YES
rw_view15 | YES
rw_view16 | YES
! (20 rows)
SELECT table_name, is_updatable, is_insertable_into
FROM information_schema.views
*************** SELECT table_name, is_updatable, is_inse
*** 73,79 ****
ro_view19 | NO | NO
ro_view2 | NO | NO
ro_view20 | NO | NO
- ro_view21 | NO | NO
ro_view3 | NO | NO
ro_view4 | NO | NO
ro_view5 | NO | NO
--- 70,75 ----
*************** SELECT table_name, is_updatable, is_inse
*** 84,90 ****
rw_view14 | YES | YES
rw_view15 | YES | YES
rw_view16 | YES | YES
! (21 rows)
SELECT table_name, column_name, is_updatable
FROM information_schema.columns
--- 80,86 ----
rw_view14 | YES | YES
rw_view15 | YES | YES
rw_view16 | YES | YES
! (20 rows)
SELECT table_name, column_name, is_updatable
FROM information_schema.columns
*************** SELECT table_name, column_name, is_updat
*** 103,125 ****
ro_view17 | a | NO
ro_view17 | b | NO
ro_view18 | a | NO
! ro_view18 | b | NO
! ro_view19 | a | NO
ro_view2 | a | NO
ro_view2 | b | NO
! ro_view20 | sequence_name | NO
! ro_view20 | last_value | NO
! ro_view20 | start_value | NO
! ro_view20 | increment_by | NO
! ro_view20 | max_value | NO
! ro_view20 | min_value | NO
! ro_view20 | cache_value | NO
! ro_view20 | log_cnt | NO
! ro_view20 | is_cycled | NO
! ro_view20 | is_called | NO
! ro_view21 | a | NO
! ro_view21 | b | NO
! ro_view21 | g | NO
ro_view3 | ?column? | NO
ro_view4 | count | NO
ro_view5 | a | NO
--- 99,119 ----
ro_view17 | a | NO
ro_view17 | b | NO
ro_view18 | a | NO
! ro_view19 | sequence_name | NO
! ro_view19 | last_value | NO
! ro_view19 | start_value | NO
! ro_view19 | increment_by | NO
! ro_view19 | max_value | NO
! ro_view19 | min_value | NO
! ro_view19 | cache_value | NO
! ro_view19 | log_cnt | NO
! ro_view19 | is_cycled | NO
! ro_view19 | is_called | NO
ro_view2 | a | NO
ro_view2 | b | NO
! ro_view20 | a | NO
! ro_view20 | b | NO
! ro_view20 | g | NO
ro_view3 | ?column? | NO
ro_view4 | count | NO
ro_view5 | a | NO
*************** SELECT table_name, column_name, is_updat
*** 140,146 ****
rw_view16 | a | YES
rw_view16 | b | YES
rw_view16 | aa | YES
! (48 rows)
-- Read-only views
DELETE FROM ro_view1;
--- 134,140 ----
rw_view16 | a | YES
rw_view16 | b | YES
rw_view16 | aa | YES
! (46 rows)
-- Read-only views
DELETE FROM ro_view1;
*************** INSERT INTO ro_view17 VALUES (3, 'ROW 3'
*** 268,291 ****
ERROR: cannot insert into view "ro_view1"
DETAIL: Views containing DISTINCT are not automatically updatable.
HINT: To enable inserting into the view, provide an INSTEAD OF INSERT trigger or an unconditional ON INSERT DO INSTEAD rule.
! INSERT INTO ro_view18 VALUES (3, 'ROW 3');
! ERROR: cannot insert into view "ro_view18"
! DETAIL: Security-barrier views are not automatically updatable.
! HINT: To enable inserting into the view, provide an INSTEAD OF INSERT trigger or an unconditional ON INSERT DO INSTEAD rule.
! DELETE FROM ro_view19;
! ERROR: cannot delete from view "ro_view19"
DETAIL: Views that do not select from a single table or view are not automatically updatable.
HINT: To enable deleting from the view, provide an INSTEAD OF DELETE trigger or an unconditional ON DELETE DO INSTEAD rule.
! UPDATE ro_view20 SET max_value=1000;
! ERROR: cannot update view "ro_view20"
DETAIL: Views that do not select from a single table or view are not automatically updatable.
HINT: To enable updating the view, provide an INSTEAD OF UPDATE trigger or an unconditional ON UPDATE DO INSTEAD rule.
! UPDATE ro_view21 SET b=upper(b);
! ERROR: cannot update view "ro_view21"
DETAIL: Views that return set-returning functions are not automatically updatable.
HINT: To enable updating the view, provide an INSTEAD OF UPDATE trigger or an unconditional ON UPDATE DO INSTEAD rule.
DROP TABLE base_tbl CASCADE;
! NOTICE: drop cascades to 17 other objects
DETAIL: drop cascades to view ro_view1
drop cascades to view ro_view17
drop cascades to view ro_view2
--- 262,281 ----
ERROR: cannot insert into view "ro_view1"
DETAIL: Views containing DISTINCT are not automatically updatable.
HINT: To enable inserting into the view, provide an INSTEAD OF INSERT trigger or an unconditional ON INSERT DO INSTEAD rule.
! DELETE FROM ro_view18;
! ERROR: cannot delete from view "ro_view18"
DETAIL: Views that do not select from a single table or view are not automatically updatable.
HINT: To enable deleting from the view, provide an INSTEAD OF DELETE trigger or an unconditional ON DELETE DO INSTEAD rule.
! UPDATE ro_view19 SET max_value=1000;
! ERROR: cannot update view "ro_view19"
DETAIL: Views that do not select from a single table or view are not automatically updatable.
HINT: To enable updating the view, provide an INSTEAD OF UPDATE trigger or an unconditional ON UPDATE DO INSTEAD rule.
! UPDATE ro_view20 SET b=upper(b);
! ERROR: cannot update view "ro_view20"
DETAIL: Views that return set-returning functions are not automatically updatable.
HINT: To enable updating the view, provide an INSTEAD OF UPDATE trigger or an unconditional ON UPDATE DO INSTEAD rule.
DROP TABLE base_tbl CASCADE;
! NOTICE: drop cascades to 16 other objects
DETAIL: drop cascades to view ro_view1
drop cascades to view ro_view17
drop cascades to view ro_view2
*************** drop cascades to view ro_view11
*** 299,311 ****
drop cascades to view ro_view13
drop cascades to view rw_view15
drop cascades to view rw_view16
! drop cascades to view ro_view18
! drop cascades to view ro_view21
drop cascades to view ro_view4
drop cascades to view rw_view14
! DROP VIEW ro_view10, ro_view12, ro_view19;
DROP SEQUENCE seq CASCADE;
! NOTICE: drop cascades to view ro_view20
-- simple updatable view
CREATE TABLE base_tbl (a int PRIMARY KEY, b text DEFAULT 'Unspecified');
INSERT INTO base_tbl SELECT i, 'Row ' || i FROM generate_series(-2, 2) g(i);
--- 289,300 ----
drop cascades to view ro_view13
drop cascades to view rw_view15
drop cascades to view rw_view16
! drop cascades to view ro_view20
drop cascades to view ro_view4
drop cascades to view rw_view14
! DROP VIEW ro_view10, ro_view12, ro_view18;
DROP SEQUENCE seq CASCADE;
! NOTICE: drop cascades to view ro_view19
-- simple updatable view
CREATE TABLE base_tbl (a int PRIMARY KEY, b text DEFAULT 'Unspecified');
INSERT INTO base_tbl SELECT i, 'Row ' || i FROM generate_series(-2, 2) g(i);
*************** INSERT INTO rw_view2 VALUES (2,3); -- ok
*** 1739,1742 ****
--- 1728,1927 ----
DROP TABLE base_tbl CASCADE;
NOTICE: drop cascades to 2 other objects
DETAIL: drop cascades to view rw_view1
+ drop cascades to view rw_view2
+ -- security barrier view
+ CREATE TABLE base_tbl (person text, visibility text);
+ INSERT INTO base_tbl VALUES ('Tom', 'public'),
+ ('Dick', 'private'),
+ ('Harry', 'public');
+ CREATE VIEW rw_view1 AS
+ SELECT person FROM base_tbl WHERE visibility = 'public';
+ CREATE FUNCTION snoop(val text)
+ RETURNS boolean AS
+ $$
+ BEGIN
+ RAISE NOTICE 'snooped value: %', val;
+ RETURN true;
+ END;
+ $$
+ LANGUAGE plpgsql COST 0.000001;
+ SELECT * FROM rw_view1 WHERE snoop(person);
+ NOTICE: snooped value: Tom
+ NOTICE: snooped value: Dick
+ NOTICE: snooped value: Harry
+ person
+ --------
+ Tom
+ Harry
+ (2 rows)
+
+ UPDATE rw_view1 SET person=person WHERE snoop(person);
+ NOTICE: snooped value: Tom
+ NOTICE: snooped value: Dick
+ NOTICE: snooped value: Harry
+ DELETE FROM rw_view1 WHERE NOT snoop(person);
+ NOTICE: snooped value: Dick
+ NOTICE: snooped value: Tom
+ NOTICE: snooped value: Harry
+ ALTER VIEW rw_view1 SET (security_barrier = true);
+ SELECT table_name, is_insertable_into
+ FROM information_schema.tables
+ WHERE table_name = 'rw_view1';
+ table_name | is_insertable_into
+ ------------+--------------------
+ rw_view1 | YES
+ (1 row)
+
+ SELECT table_name, is_updatable, is_insertable_into
+ FROM information_schema.views
+ WHERE table_name = 'rw_view1';
+ table_name | is_updatable | is_insertable_into
+ ------------+--------------+--------------------
+ rw_view1 | YES | YES
+ (1 row)
+
+ SELECT table_name, column_name, is_updatable
+ FROM information_schema.columns
+ WHERE table_name = 'rw_view1'
+ ORDER BY ordinal_position;
+ table_name | column_name | is_updatable
+ ------------+-------------+--------------
+ rw_view1 | person | YES
+ (1 row)
+
+ SELECT * FROM rw_view1 WHERE snoop(person);
+ NOTICE: snooped value: Tom
+ NOTICE: snooped value: Harry
+ person
+ --------
+ Tom
+ Harry
+ (2 rows)
+
+ UPDATE rw_view1 SET person=person WHERE snoop(person);
+ NOTICE: snooped value: Tom
+ NOTICE: snooped value: Harry
+ DELETE FROM rw_view1 WHERE NOT snoop(person);
+ NOTICE: snooped value: Tom
+ NOTICE: snooped value: Harry
+ EXPLAIN (costs off) SELECT * FROM rw_view1 WHERE snoop(person);
+ QUERY PLAN
+ -----------------------------------------------
+ Subquery Scan on rw_view1
+ Filter: snoop(rw_view1.person)
+ -> Seq Scan on base_tbl
+ Filter: (visibility = 'public'::text)
+ (4 rows)
+
+ EXPLAIN (costs off) UPDATE rw_view1 SET person=person WHERE snoop(person);
+ QUERY PLAN
+ -----------------------------------------------------------
+ Update on base_tbl base_tbl_1
+ -> Subquery Scan on base_tbl
+ Filter: snoop(base_tbl.person)
+ -> LockRows
+ -> Seq Scan on base_tbl base_tbl_2
+ Filter: (visibility = 'public'::text)
+ (6 rows)
+
+ EXPLAIN (costs off) DELETE FROM rw_view1 WHERE NOT snoop(person);
+ QUERY PLAN
+ -----------------------------------------------------------
+ Delete on base_tbl base_tbl_1
+ -> Subquery Scan on base_tbl
+ Filter: (NOT snoop(base_tbl.person))
+ -> LockRows
+ -> Seq Scan on base_tbl base_tbl_2
+ Filter: (visibility = 'public'::text)
+ (6 rows)
+
+ -- security barrier view on top of security barrier view
+ CREATE VIEW rw_view2 WITH (security_barrier = true) AS
+ SELECT * FROM rw_view1 WHERE snoop(person);
+ SELECT table_name, is_insertable_into
+ FROM information_schema.tables
+ WHERE table_name = 'rw_view2';
+ table_name | is_insertable_into
+ ------------+--------------------
+ rw_view2 | YES
+ (1 row)
+
+ SELECT table_name, is_updatable, is_insertable_into
+ FROM information_schema.views
+ WHERE table_name = 'rw_view2';
+ table_name | is_updatable | is_insertable_into
+ ------------+--------------+--------------------
+ rw_view2 | YES | YES
+ (1 row)
+
+ SELECT table_name, column_name, is_updatable
+ FROM information_schema.columns
+ WHERE table_name = 'rw_view2'
+ ORDER BY ordinal_position;
+ table_name | column_name | is_updatable
+ ------------+-------------+--------------
+ rw_view2 | person | YES
+ (1 row)
+
+ SELECT * FROM rw_view2 WHERE snoop(person);
+ NOTICE: snooped value: Tom
+ NOTICE: snooped value: Tom
+ NOTICE: snooped value: Harry
+ NOTICE: snooped value: Harry
+ person
+ --------
+ Tom
+ Harry
+ (2 rows)
+
+ UPDATE rw_view2 SET person=person WHERE snoop(person);
+ NOTICE: snooped value: Tom
+ NOTICE: snooped value: Tom
+ NOTICE: snooped value: Harry
+ NOTICE: snooped value: Harry
+ DELETE FROM rw_view2 WHERE NOT snoop(person);
+ NOTICE: snooped value: Tom
+ NOTICE: snooped value: Tom
+ NOTICE: snooped value: Harry
+ NOTICE: snooped value: Harry
+ EXPLAIN (costs off) SELECT * FROM rw_view2 WHERE snoop(person);
+ QUERY PLAN
+ -----------------------------------------------------
+ Subquery Scan on rw_view2
+ Filter: snoop(rw_view2.person)
+ -> Subquery Scan on rw_view1
+ Filter: snoop(rw_view1.person)
+ -> Seq Scan on base_tbl
+ Filter: (visibility = 'public'::text)
+ (6 rows)
+
+ EXPLAIN (costs off) UPDATE rw_view2 SET person=person WHERE snoop(person);
+ QUERY PLAN
+ -----------------------------------------------------------------
+ Update on base_tbl base_tbl_1
+ -> Subquery Scan on base_tbl
+ Filter: snoop(base_tbl.person)
+ -> Subquery Scan on base_tbl_2
+ Filter: snoop(base_tbl_2.person)
+ -> LockRows
+ -> Seq Scan on base_tbl base_tbl_3
+ Filter: (visibility = 'public'::text)
+ (8 rows)
+
+ EXPLAIN (costs off) DELETE FROM rw_view2 WHERE NOT snoop(person);
+ QUERY PLAN
+ -----------------------------------------------------------------
+ Delete on base_tbl base_tbl_1
+ -> Subquery Scan on base_tbl
+ Filter: (NOT snoop(base_tbl.person))
+ -> Subquery Scan on base_tbl_2
+ Filter: snoop(base_tbl_2.person)
+ -> LockRows
+ -> Seq Scan on base_tbl base_tbl_3
+ Filter: (visibility = 'public'::text)
+ (8 rows)
+
+ DROP TABLE base_tbl CASCADE;
+ NOTICE: drop cascades to 2 other objects
+ DETAIL: drop cascades to view rw_view1
drop cascades to view rw_view2
diff --git a/src/test/regress/sql/updatable_views.sql b/src/test/regress/sql/updatable_views.sql
new file mode 100644
index a77cf19..6b352c1
*** a/src/test/regress/sql/updatable_views.sql
--- b/src/test/regress/sql/updatable_views.sql
*************** CREATE VIEW rw_view14 AS SELECT ctid, a,
*** 25,36 ****
CREATE VIEW rw_view15 AS SELECT a, upper(b) FROM base_tbl; -- Expression/function may be part of an updatable view
CREATE VIEW rw_view16 AS SELECT a, b, a AS aa FROM base_tbl; -- Repeated column may be part of an updatable view
CREATE VIEW ro_view17 AS SELECT * FROM ro_view1; -- Base relation not updatable
! CREATE VIEW ro_view18 WITH (security_barrier = true)
! AS SELECT * FROM base_tbl; -- Security barrier views not updatable
! CREATE VIEW ro_view19 AS SELECT * FROM (VALUES(1)) AS tmp(a); -- VALUES in rangetable
CREATE SEQUENCE seq;
! CREATE VIEW ro_view20 AS SELECT * FROM seq; -- View based on a sequence
! CREATE VIEW ro_view21 AS SELECT a, b, generate_series(1, a) g FROM base_tbl; -- SRF in targetlist not supported
SELECT table_name, is_insertable_into
FROM information_schema.tables
--- 25,34 ----
CREATE VIEW rw_view15 AS SELECT a, upper(b) FROM base_tbl; -- Expression/function may be part of an updatable view
CREATE VIEW rw_view16 AS SELECT a, b, a AS aa FROM base_tbl; -- Repeated column may be part of an updatable view
CREATE VIEW ro_view17 AS SELECT * FROM ro_view1; -- Base relation not updatable
! CREATE VIEW ro_view18 AS SELECT * FROM (VALUES(1)) AS tmp(a); -- VALUES in rangetable
CREATE SEQUENCE seq;
! CREATE VIEW ro_view19 AS SELECT * FROM seq; -- View based on a sequence
! CREATE VIEW ro_view20 AS SELECT a, b, generate_series(1, a) g FROM base_tbl; -- SRF in targetlist not supported
SELECT table_name, is_insertable_into
FROM information_schema.tables
*************** SELECT * FROM base_tbl;
*** 87,99 ****
DELETE FROM rw_view16 WHERE a=-3; -- should be OK
-- Read-only views
INSERT INTO ro_view17 VALUES (3, 'ROW 3');
! INSERT INTO ro_view18 VALUES (3, 'ROW 3');
! DELETE FROM ro_view19;
! UPDATE ro_view20 SET max_value=1000;
! UPDATE ro_view21 SET b=upper(b);
DROP TABLE base_tbl CASCADE;
! DROP VIEW ro_view10, ro_view12, ro_view19;
DROP SEQUENCE seq CASCADE;
-- simple updatable view
--- 85,96 ----
DELETE FROM rw_view16 WHERE a=-3; -- should be OK
-- Read-only views
INSERT INTO ro_view17 VALUES (3, 'ROW 3');
! DELETE FROM ro_view18;
! UPDATE ro_view19 SET max_value=1000;
! UPDATE ro_view20 SET b=upper(b);
DROP TABLE base_tbl CASCADE;
! DROP VIEW ro_view10, ro_view12, ro_view18;
DROP SEQUENCE seq CASCADE;
-- simple updatable view
*************** CREATE VIEW rw_view2 AS
*** 828,830 ****
--- 825,902 ----
SELECT * FROM rw_view1 WHERE a > b WITH LOCAL CHECK OPTION;
INSERT INTO rw_view2 VALUES (2,3); -- ok, but not in view (doesn't fail rw_view2's check)
DROP TABLE base_tbl CASCADE;
+
+ -- security barrier view
+
+ CREATE TABLE base_tbl (person text, visibility text);
+ INSERT INTO base_tbl VALUES ('Tom', 'public'),
+ ('Dick', 'private'),
+ ('Harry', 'public');
+
+ CREATE VIEW rw_view1 AS
+ SELECT person FROM base_tbl WHERE visibility = 'public';
+
+ CREATE FUNCTION snoop(val text)
+ RETURNS boolean AS
+ $$
+ BEGIN
+ RAISE NOTICE 'snooped value: %', val;
+ RETURN true;
+ END;
+ $$
+ LANGUAGE plpgsql COST 0.000001;
+
+ SELECT * FROM rw_view1 WHERE snoop(person);
+ UPDATE rw_view1 SET person=person WHERE snoop(person);
+ DELETE FROM rw_view1 WHERE NOT snoop(person);
+
+ ALTER VIEW rw_view1 SET (security_barrier = true);
+
+ SELECT table_name, is_insertable_into
+ FROM information_schema.tables
+ WHERE table_name = 'rw_view1';
+
+ SELECT table_name, is_updatable, is_insertable_into
+ FROM information_schema.views
+ WHERE table_name = 'rw_view1';
+
+ SELECT table_name, column_name, is_updatable
+ FROM information_schema.columns
+ WHERE table_name = 'rw_view1'
+ ORDER BY ordinal_position;
+
+ SELECT * FROM rw_view1 WHERE snoop(person);
+ UPDATE rw_view1 SET person=person WHERE snoop(person);
+ DELETE FROM rw_view1 WHERE NOT snoop(person);
+
+ EXPLAIN (costs off) SELECT * FROM rw_view1 WHERE snoop(person);
+ EXPLAIN (costs off) UPDATE rw_view1 SET person=person WHERE snoop(person);
+ EXPLAIN (costs off) DELETE FROM rw_view1 WHERE NOT snoop(person);
+
+ -- security barrier view on top of security barrier view
+
+ CREATE VIEW rw_view2 WITH (security_barrier = true) AS
+ SELECT * FROM rw_view1 WHERE snoop(person);
+
+ SELECT table_name, is_insertable_into
+ FROM information_schema.tables
+ WHERE table_name = 'rw_view2';
+
+ SELECT table_name, is_updatable, is_insertable_into
+ FROM information_schema.views
+ WHERE table_name = 'rw_view2';
+
+ SELECT table_name, column_name, is_updatable
+ FROM information_schema.columns
+ WHERE table_name = 'rw_view2'
+ ORDER BY ordinal_position;
+
+ SELECT * FROM rw_view2 WHERE snoop(person);
+ UPDATE rw_view2 SET person=person WHERE snoop(person);
+ DELETE FROM rw_view2 WHERE NOT snoop(person);
+
+ EXPLAIN (costs off) SELECT * FROM rw_view2 WHERE snoop(person);
+ EXPLAIN (costs off) UPDATE rw_view2 SET person=person WHERE snoop(person);
+ EXPLAIN (costs off) DELETE FROM rw_view2 WHERE NOT snoop(person);
+
+ DROP TABLE base_tbl CASCADE;
On 11/06/2013 05:02 PM, Dean Rasheed wrote:
The basic idea is to have rewriteTargetView() collect up any quals
from SB views in a new list on the target RTE, instead of adding them
to the main query's predicates (it needs to be a list of SB quals, in
case there are SB views on top of other SB views, in which case they
need to be kept separate from one another). Then at the end of the
rewriting process (after any views referenced in the SB quals have
been expanded), a new piece of code kicks in to process any RTEs with
SB quals, turning them into (possibly nested) subquery RTEs.
That makes sense, though presumably you face the same problem that the
existing RLS code does with references to system columns that don't
normally exist in subqueries?
Since this happens during query rewrite, what prevents the optimizer
from pushing outer quals down into the subqueries?
The complication is that the query's resultRelation RTE mustn't be a
subquery.
I think this is what Robert was alluding to earlier with his comments
about join relations:
____
Robert Haas wrote:
I don't really see why. AIUI, the ModifyTable node just needs to get
the right TIDs. It's not like that has to be stacked directly on top
of a scan; indeed, in cases like UPDATE .. FROM and DELETE .. USING it
already isn't. Maybe there's some reason why the intervening level
can be a Join but not a SubqueryScan, but if so I'd expect we could
find some way of lifting that limitation without suffering too much
pain.
(/messages/by-id/CA+TgmoYr1PHw3X9vnVuWDcfXkzK2p_jhtWc0fV2Q58NEgcxyTA@mail.gmail.com)
____
Maybe we just need to make a subquery scan a valid target for an update,
so those fixups aren't required anymore?
This is handled this in a similar way to the
trigger-updatable views code, producing 2 RTEs --- the resultRelation
RTE becomes a direct reference to the base relation, and a separate
subquery RTE acts as the source of rows to update.
Some of the complexity of the current RLS code is caused by the need to
do similar fixups to handle the case where the input relation isn't the
same as the target relation, but is a subquery over it instead.
Anyway, feel free to do what you like with this. I wasn't planning on
submitting it to the next commitfest myself, because my non-PG
workload is too high, and I don't expect to get much time to hack on
postgresql during the next couple of months.
Thanks for sending what you have. It's informative, and it shows that
some of the same issues must be solved for writable security barrier
views and for RLS.
--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 6 November 2013 09:23, Craig Ringer <craig@2ndquadrant.com> wrote:
On 11/06/2013 05:02 PM, Dean Rasheed wrote:
The basic idea is to have rewriteTargetView() collect up any quals
from SB views in a new list on the target RTE, instead of adding them
to the main query's predicates (it needs to be a list of SB quals, in
case there are SB views on top of other SB views, in which case they
need to be kept separate from one another). Then at the end of the
rewriting process (after any views referenced in the SB quals have
been expanded), a new piece of code kicks in to process any RTEs with
SB quals, turning them into (possibly nested) subquery RTEs.That makes sense, though presumably you face the same problem that the
existing RLS code does with references to system columns that don't
normally exist in subqueries?
Yeah, that feels like an ugly hack.
Since this happens during query rewrite, what prevents the optimizer
from pushing outer quals down into the subqueries?
The subquery RTE is marked with the security_barrier flag, which
prevents quals from being pushed down in the presence of leaky
functions (see set_subquery_pathlist).
The complication is that the query's resultRelation RTE mustn't be a
subquery.I think this is what Robert was alluding to earlier with his comments
about join relations:____
Robert Haas wrote:I don't really see why. AIUI, the ModifyTable node just needs to get
the right TIDs. It's not like that has to be stacked directly on top
of a scan; indeed, in cases like UPDATE .. FROM and DELETE .. USING it
already isn't. Maybe there's some reason why the intervening level
can be a Join but not a SubqueryScan, but if so I'd expect we could
find some way of lifting that limitation without suffering too much
pain.(/messages/by-id/CA+TgmoYr1PHw3X9vnVuWDcfXkzK2p_jhtWc0fV2Q58NEgcxyTA@mail.gmail.com)
____
Yeah. We already do a similar thing for trigger-updatable views. So
with this approach you end up with similar plans, for example:
Update on base_tbl
-> Subquery Scan on base_tbl ...
Maybe we just need to make a subquery scan a valid target for an update,
so those fixups aren't required anymore?
Possibly. That feels like it would require much more extensive surgery
on the planner though. I've not explored that idea, but I suspect it
would quickly turn into a whole new can of worms.
This is handled this in a similar way to the
trigger-updatable views code, producing 2 RTEs --- the resultRelation
RTE becomes a direct reference to the base relation, and a separate
subquery RTE acts as the source of rows to update.Some of the complexity of the current RLS code is caused by the need to
do similar fixups to handle the case where the input relation isn't the
same as the target relation, but is a subquery over it instead.Anyway, feel free to do what you like with this. I wasn't planning on
submitting it to the next commitfest myself, because my non-PG
workload is too high, and I don't expect to get much time to hack on
postgresql during the next couple of months.Thanks for sending what you have. It's informative, and it shows that
some of the same issues must be solved for writable security barrier
views and for RLS.
Agreed. I'm not sure what the best way to fix those issues is though.
The currently proposed approach feels pretty ugly, but I can't see a
better way at the moment.
Regards,
Dean
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
All,
Just a comment: I'm really glad to see the serious work on this. If RLS
doesn't make it into 9.4, it'll be because the problems of RLS are
fundamentally unsolvable, not because we didn't give it our best. Great
work, all!
--
Josh Berkus
PostgreSQL Experts Inc.
http://pgexperts.com
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Import Notes
Reply to msg id not found: WM6efc4c8c32e88badd24b0478b03b50d8216fbfb54f8fc91b01c3d2a4cf2e345832b78317000eed2d89cc8e471ae7259d@asav-3.01.com
2013/11/6 Craig Ringer <craig@2ndquadrant.com>:
On 11/05/2013 09:36 PM, Robert Haas wrote:
I haven't studied this patch in detail, but I see why there's some
unhappiness about that code: it's an RLS-specific kluge. Just
shooting from the hip here, maybe we should attack the problem of
making security-barrier views updatable first, as a separate patch.That's the approach I've been considering. There are a few wrinkles with
it, though:(a) Updatable views are implemented in the rewriter, not the planner.
The rewriter is not re-run when plans are invalidated or when the
session authorization changes, etc. This means that we can't simply omit
the RLS predicate for superuser because the same rewritten parse tree
might get used for both superuser and non-superuser queries.Options:
* Store the before-rewrite parse tree when RLS is discovered on one of
the rels in the tree. Re-run the rewriter when re-planning. Ensure a
change in current user always invalidates plans.* Declare that it's not legal to run a query originally parsed and
rewritten as superuser as a non-superuser or vice versa. This would
cause a great deal of pain with PL/PgSQL.* Always add the RLS predicate and solve the problem of reliably
short-circuiting the user-supplied predicate for RLS-exempt users. We'd
need a way to allow direct (not query-based) COPY TO for tables with RLS
applied, preventing the rewriting of direct table access into subqueries
for COPY, but since we never save plans for COPY that may be fine.* ... ?
How about an idea that uses two different type of rules: the existing one
is expanded prior to planner stage as we are doing now, and the newer
one is expanded on the head of planner stage.
The argument of planner() is still parse tree, so it seems to me here is
no serious problem to call rewriter again to handle second stage rules.
If we go on this approach, ALTER TABLE ... SET ROW SECURITY
will become a synonym to declare a rule with special attribute.
(b) Inheritance is a problem when RLS is done in the rewriter. As I
understood it from Kohei KaiGai's description to me earlier, there was a
strong preference on -hackers to enforce RLS predicates for child and
parent tables completely independently. That's how RLS currently works,
but it might be hard to get the same effect when applying RLS in the
rewriter. We'd need to solve that, or redefine RLS's behaviour so that
the predicate on a parent table applies to any child tables too.
Personally I'd prefer the latter.
I'm not certain whether it was a "strong preference", even though I followed
the consensus at that time. So, I think it makes sense to discuss how RLS
policy shall be enforced on the child tables.
As long as we can have consistent view on child tables even if it is referenced
without parent tables, I don't have any arguments to your preference.
Also, it makes implementation simple than the approach I tried to have; that
enforces RLS policy of tables individually, because of utilization of existing
rule mechanism.
It is not difficult to enforce parent's RLS policy on the child relation even if
it is referenced individually. All we need to do special is append RLS policy
of its parent, not only child's one, if referenced table has parent.
(c) RLS might interact differently with rules declared on tables if
implemented in the rewriter, so some investigation into that would be
needed.I
would think that if we make that work, this will also work without,
hopefully, any special hackery. And we'd get a separate,
independently useful feature out of it, too.I tend to agree. I'm just a bit concerned about dealing with the issues
around RLS-exempt operations and users.--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
--
KaiGai Kohei <kaigai@kaigai.gr.jp>
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 6 November 2013 19:12, Kohei KaiGai <kaigai@kaigai.gr.jp> wrote:
2013/11/6 Craig Ringer <craig@2ndquadrant.com>:
On 11/05/2013 09:36 PM, Robert Haas wrote:
I haven't studied this patch in detail, but I see why there's some
unhappiness about that code: it's an RLS-specific kluge. Just
shooting from the hip here, maybe we should attack the problem of
making security-barrier views updatable first, as a separate patch.That's the approach I've been considering. There are a few wrinkles with
it, though:(a) Updatable views are implemented in the rewriter, not the planner.
The rewriter is not re-run when plans are invalidated or when the
session authorization changes, etc. This means that we can't simply omit
the RLS predicate for superuser because the same rewritten parse tree
might get used for both superuser and non-superuser queries.Options:
* Store the before-rewrite parse tree when RLS is discovered on one of
the rels in the tree. Re-run the rewriter when re-planning. Ensure a
change in current user always invalidates plans.* Declare that it's not legal to run a query originally parsed and
rewritten as superuser as a non-superuser or vice versa. This would
cause a great deal of pain with PL/PgSQL.* Always add the RLS predicate and solve the problem of reliably
short-circuiting the user-supplied predicate for RLS-exempt users. We'd
need a way to allow direct (not query-based) COPY TO for tables with RLS
applied, preventing the rewriting of direct table access into subqueries
for COPY, but since we never save plans for COPY that may be fine.* ... ?
How about an idea that uses two different type of rules: the existing one
is expanded prior to planner stage as we are doing now, and the newer
one is expanded on the head of planner stage.
The argument of planner() is still parse tree, so it seems to me here is
no serious problem to call rewriter again to handle second stage rules.
If we go on this approach, ALTER TABLE ... SET ROW SECURITY
will become a synonym to declare a rule with special attribute.
I don't really get this part of the discussion. Why would you want to
make updatable SB views do any of that? With RLS on tables, there is
only one object in play - the table itself. So I can see that there is
this requirement for certain privileged users to be able to bypass the
RLS quals, and hence the need to invalidate cached plans.
With SB views, however, you can have multiple SB views on top of the
same base table, each giving different users access to different
subsets of the data, and controlled by suitable GRANTs, and suitably
privileged users can be given direct access to the base table. This
also gives greater flexibility than the superuser/non-superuser
distinction being discussed here.
I don't think a view should ever show different data to different
users (unless it has been deliberately set up to do so) because that
would likely lead to confusion. Is there some other use-case that I'm
missing here?
Regards,
Dean
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Wed, Nov 6, 2013 at 6:38 AM, Craig Ringer <craig@2ndquadrant.com> wrote:
That's the approach I've been considering. There are a few wrinkles with
it, though:(a) Updatable views are implemented in the rewriter, not the planner.
The rewriter is not re-run when plans are invalidated or when the
session authorization changes, etc. This means that we can't simply omit
the RLS predicate for superuser because the same rewritten parse tree
might get used for both superuser and non-superuser queries.
Incidentally I still feel this is at root the problem with updateable views
in general. I know it's a bit off to be tossing in concerns from the peanut
gallery when I'm not actually offering to work on it and others are having
putting in serious efforts in this area and having some success. So take
this for what it's worth...
I think the right approach for updateable views would be to support a
syntax like this in the planner:
UPDATE (select * from tab1,tab2 ...) WHERE tab1.id = .. SET ...
Then updateable views would just rewrite the query mechanically the way
regular views work by substituting the view definition in place of the view
name. Since all the work would be done in the planner it would have access
to the same kinds of information that regular join planning etc have.
I'm not sure if this solves all the problems with RLS but it would solve
the concern about plan invalidations and I think it would make it simpler
to reason about security rules that are otherwise happening at plan time.
--
greg
On 11/07/2013 09:47 PM, Greg Stark wrote:
Incidentally I still feel this is at root the problem with updateable
views in general. I know it's a bit off to be tossing in concerns from
the peanut gallery when I'm not actually offering to work on it and
others are having putting in serious efforts in this area and having
some success. So take this for what it's worth...
Frankly, the peanut gallery input can be quite handy. It's easy to get
so stuck in the way you've seen it thought about already that you don't
see other ways to view it. Plus, sometimes the peanut gallery becomes
the "oh, I don't like this at all" crowd when commit time is
approaching, so early comments are better than no comments then last
minute complaints.
I think the right approach for updateable views would be to support a
syntax like this in the planner:UPDATE (select * from tab1,tab2 ...) WHERE tab1.id <http://tab1.id> = ..
SET ...
I want to support that for rewritten parse trees, and therefore (because
of recursive rewrite) in pre-rewrite parse trees. It's exactly what's
needed to make this sane, IMO, and I think this is what Robert was
suggesting with making UPDATE capable of dealing with operating directly
on a subquery scan.
I'm not at all convinced it should be exposed to the user and accepted
by the parser as SQL, but I don't know if that's what you were suggesting.
Robert? Is this what you meant? If so, any chance you can point a
planner neophyte like me in vaguely the right direction?
I'm not sure if this solves all the problems with RLS but it would solve
the concern about plan invalidations and I think it would make it
simpler to reason about security rules that are otherwise happening at
plan time.
I don't think it'd help with plan invalidations. View expansion happens
at rewrite time, and we don't re-run the rewriter on the original parse
tree when we invalidate plans and re-plan. The current patch entirely
elides the RLS quals and subquery when running as superuser and that
won't work with view expansion at the rewrite stage.
So we still have that to deal with, and need to handle a few side issues
with portals and user id changes etc, but the biggest issue is coming up
with an acceptable way to update a security barrier view or subquery.
--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 11/07/2013 06:11 PM, Dean Rasheed wrote:
I don't really get this part of the discussion. Why would you want to
make updatable SB views do any of that?
I don't, especially. If we're going to use updatable SB views as the
basis for RLS then we need the option to skip adding the qual for superuser.
That might be done outside the SB code its self, by not invoking the SB
view rewrite on the base rel when we see we're running as an RLS-exempt
user. It's all about dealing with the re-plan problem really.
With SB views, however, you can have multiple SB views on top of the
same base table, each giving different users access to different
subsets of the data, and controlled by suitable GRANTs, and suitably
privileged users can be given direct access to the base table. This
also gives greater flexibility than the superuser/non-superuser
distinction being discussed here.
That's a thought.
Can we munge what the planner sees in pg_class instead? So when we see a
ref to an RLS relation we transparently substitute the oid for a hidden
SB view over the relation instead. For RLS exempt users we omit that
substitution.
Since the lookups and view subs are done in the rewrite phase it
probably doesn't help us much, but it'd get rid of the need to play
about with substituting a RTE_RELATION with an RTE_SUBQUERY dynamically
during rewrite.
I don't think a view should ever show different data to different
users (unless it has been deliberately set up to do so) because that
would likely lead to confusion. Is there some other use-case that I'm
missing here?
The main concern is pg_dump - it's important that dumps be able to take
a complete copy without relying on hacks or bug-free user-written RLS quals.
Highly privileged users should also be exempt from RLS so they don't
invoke untrusted functions that're part of RLS quals written by
lower-rights users.
This isn't really "superuser" vs "not superuser. In fact we'll want a
new right that controls whether RLS can be bypassed, and another that
controls the ability to set RLS rights on tables. Both of those would be
superuser only by default, but could be delegated.
(Note: It's important that table owners _not_ get the right to set RLS
on tables by default for security reasons. I'll explain in more detail
later.)
--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Wed, Nov 6, 2013 at 1:38 AM, Craig Ringer <craig@2ndquadrant.com> wrote:
(a) Updatable views are implemented in the rewriter, not the planner.
The rewriter is not re-run when plans are invalidated or when the
session authorization changes, etc. This means that we can't simply omit
the RLS predicate for superuser because the same rewritten parse tree
might get used for both superuser and non-superuser queries.
My impression was that we had discussed this problem with respect to
some earlier version of the RLS patch and that the conclusion of that
discussion was that we needed to record in the cached plan whether the
plan was one which is sensitive to the user ID and, if so, avoid using
that plan with a different user ID. I am murky on the details; I
believe the original discussion of this topic was a year or more ago.
(b) Inheritance is a problem when RLS is done in the rewriter. As I
understood it from Kohei KaiGai's description to me earlier, there was a
strong preference on -hackers to enforce RLS predicates for child and
parent tables completely independently.
Not to put a too fine a point on it, but I think that's a really bad
plan; and I don't remember any such discussion.
That's how RLS currently works,
but it might be hard to get the same effect when applying RLS in the
rewriter. We'd need to solve that, or redefine RLS's behaviour so that
the predicate on a parent table applies to any child tables too.
Personally I'd prefer the latter.
Yes, let's please redefine it. The goal here ought to be to make RLS
work as smoothly as possible with the rest of the system, not to
invent weird semantics that are both unlike what we do elsewhere - and
difficult to implement, to boot. I thought the whole point of
implementing security barrier views was that read-side RLS would work
just the same way, not having randomly incompatible semantics.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Thu, Nov 7, 2013 at 9:08 PM, Craig Ringer <craig@2ndquadrant.com> wrote:
On 11/07/2013 09:47 PM, Greg Stark wrote:
Incidentally I still feel this is at root the problem with updateable
views in general. I know it's a bit off to be tossing in concerns from
the peanut gallery when I'm not actually offering to work on it and
others are having putting in serious efforts in this area and having
some success. So take this for what it's worth...Frankly, the peanut gallery input can be quite handy. It's easy to get
so stuck in the way you've seen it thought about already that you don't
see other ways to view it. Plus, sometimes the peanut gallery becomes
the "oh, I don't like this at all" crowd when commit time is
approaching, so early comments are better than no comments then last
minute complaints.I think the right approach for updateable views would be to support a
syntax like this in the planner:UPDATE (select * from tab1,tab2 ...) WHERE tab1.id <http://tab1.id> = ..
SET ...I want to support that for rewritten parse trees, and therefore (because
of recursive rewrite) in pre-rewrite parse trees. It's exactly what's
needed to make this sane, IMO, and I think this is what Robert was
suggesting with making UPDATE capable of dealing with operating directly
on a subquery scan.I'm not at all convinced it should be exposed to the user and accepted
by the parser as SQL, but I don't know if that's what you were suggesting.Robert? Is this what you meant? If so, any chance you can point a
planner neophyte like me in vaguely the right direction?
I haven't studied this issue well enough to know what's really needed
here, but Dean Rasheed's approach sounded like a promising tack to me.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
While Craig has been soldiering onward to the write side of things, I've
been working with some groups who are evaluating if the RLS feature is
good enough for switching some real world apps over to use PostgreSQL.
Amazingly I can even say that these are applications involving the US
Department of Defense, and they're going well so far. There has been
some concern on this list as to whether this feature is being evaluated
by people who are serious about real-world database security issues.
The crowd I'm talking to now sure is. There's some interesting bad
news, like how we're going to have to completely refactor all the "if
(!superuser())" shortcuts into real permissions one day to make them
happy. But for the most part the prototype conversions have been
working out. The RLS feature set available with the CF submission is
good enough for those projects to continue exploring PostgreSQL. I feel
good now that if the code issues around what's already been developed
are sorted out to commit quality, the users really will queue once this
hits.
== Review demo code ==
Attached is a small demo piece built by my new co-worker Jeff McCormick
that I've been hacking lightly on. The idea was to get something a step
beyond trivial that maps into real-world need, well enough that we could
get people to agree it was usefully representative. What they did is
take the sort of security levels these apps have, which often includes
two different criteria for each row, and mapped that into what the RLS
submission supports. There are a set of roles for the permission to see
various shapes (triangle/circle/square/heart) and colors, and then
people can see a row only if they have both color and shape
permissions. The part that I thought was cute when I first read it is
how this query looks up data in the catalog to accomplish that:
ALTER TABLE colors_shapes
SET ROW SECURITY FOR ALL TO (
color IN (
SELECT permission_role.rolname FROM pg_roles active_role,
pg_auth_members, pg_roles permission_role WHERE
active_role.rolname = current_user AND
active_role.oid = pg_auth_members.member AND
permission_role.oid = pg_auth_members.roleid
)
AND shape IN (
SELECT permission_role.rolname FROM pg_roles active_role,
pg_auth_members, pg_roles permission_role WHERE
active_role.rolname = current_user AND
active_role.oid = pg_auth_members.member AND
permission_role.oid = pg_auth_members.roleid
)
);
Insert some data that looks like this:
id | color | shape
----+-------------+---------------
1 | blue_role | triangle_role
2 | red_role | heart_role
3 | purple_role | circle_role
And then users can see rows only if they have a pair of matches on both
those role sets. The fact that you can just plug in whatever you want
into SET ROW SECURITY allows all kinds of things. I have a more
complicated sample we're still poking at, using a full pl/pgsql function
there, but I've already got enough to try and cover in this message to
bring that in today. One reason the users I've been talking to feel
comfortable that they'll be able to build apps usefully with this
feature is this interface. It feels more like a pluggable API for
defining rules than a specific implementation. Congratulations to the
usual community members who expect complete feature overkill in every
design.
== Code review ==
Onto coding. Rather than trying to cope with patches I pulled KaiGai's
tree from https://github.com/kaigai/sepgsql/tree/rowsec to try things
out. That hasn't been merged against head in a while and it's suffered
serious but not terminal bit rot. The REPLICA IDENTITY changes in
particular touched adjacent code all over the place, since it and RLS
are both added things to the end of existing lists. The merges to get
thing back to working again were only hard when my eyes crossed on the
always fun pg_class bootstrapping table. I did all that myself up to
committed changes on Dec 3. The only real change I made was to punt the
OID number assignment into a whole different range, to make future merge
rot less likely. Those are going to get reassigned at commit time
anyway, no need to try and guess the appropriate next number for each today.
I just pushed a fork of KaiGai's repo to
https://github.com/gregs1104/sepgsql/tree/rls that includes all of the
must fix items to get the feature working again. That's what the
attached demo code was tested again. The regression tests are still
seriously busted, beyond just regenerating new output. I pushed that
off for now, I need to study recent commits better to catch up on what
changed. For community reference I'm attaching an updated patch that
forks master after dfd5151c5800448a2be521797f95e1aa63d87b67 and then has
the merging I did. I'm hoping that getting a positive functional review
will lure KaiGai back to help me update things. Hopefully pulling or
looking at commits from my merge attempt helps with that.
The (blue) elephant in the room here for this feature is the list of
committers in particular who are very picky about the parts of the code
this feature is touching (*cough* Tom). One reason I'm trying to get
these demo pieces together--Jeff here is building a whole RLS test
sample repo--is to try and make the job of testing this out easier. As
I dig myself out of the hole I'm in after getting really sick instead of
going to PG.EU, I've got a whole lot of time set aside to work on
community features lined up.
== Outstanding issues ==
Things I can already see to work on here are:
-Fix the regression tests
-Catch up to master again
-Continue to expand the functional tests
-Is there enough information about row security available in psql
output? For example, there's nothing in "\d" output that suggests it
might exist. pg_rowsecurity is a monster to try and read.
-Documentation needs plenty of editing, which I can take care of.
== Cover Channels ==
One last topic since I know it's been a sore point. Covert channels.
There's still some obvious ones around. In this example I was plenty
uneasy about some of what you can learn from the EXPLAIN output too.
But no one I've talked to about deploying PostgreSQL RLS seem terrible
uncomfortable with the exposure at this point. RLS is just one layer of
extra protection on checks that are also happening in the application
API exposing the data to users. No one expects any one layer to be the
impenetrable one. In the interest of CYA, I've talked through some
scenarios where attackers have resources like a hacked libpq client and
enough info to search the key space. Instead of expecting that the
database can survive that, which is pretty unreasonable, what I'm
getting requests for now is better audit logging to make it easier to
detect that. We should certainly close any holes we can, but where
things are at today doesn't seem functionally unacceptable anymore.
--
Greg Smith greg.smith@crunchydatasolutions.com
Chief PostgreSQL Evangelist - http://crunchydatasolutions.com/
Attachments:
rls-gs-1.patchtext/plain; charset=UTF-8; name=rls-gs-1.patch; x-mac-creator=0; x-mac-type=0Download
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
new file mode 100644
index acc261c..631c5d2
*** a/doc/src/sgml/catalogs.sgml
--- b/doc/src/sgml/catalogs.sgml
***************
*** 234,239 ****
--- 234,244 ----
</row>
<row>
+ <entry><link linkend="catalog-pg-rowsecurity"><structname>pg_rowsecuritylevelsec</structname></link></entry>
+ <entry>row-level security policy of relation</entry>
+ </row>
+
+ <row>
<entry><link linkend="catalog-pg-seclabel"><structname>pg_seclabel</structname></link></entry>
<entry>security labels on database objects</entry>
</row>
***************
*** 1855,1860 ****
--- 1860,1875 ----
</row>
<row>
+ <entry><structfield>relhasrowsecurity</structfield></entry>
+ <entry><type>bool</type></entry>
+ <entry></entry>
+ <entry>
+ True if table has row-security policy; see
+ <link linkend="catalog-pg-rowsecurity"><structname>pg_rowsecurity</structname></link> catalog
+ </entry>
+ </row>
+
+ <row>
<entry><structfield>relhassubclass</structfield></entry>
<entry><type>bool</type></entry>
<entry></entry>
***************
*** 5135,5140 ****
--- 5150,5205 ----
</sect1>
+ <sect1 id="catalog-pg-rowsecurity">
+ <title><structname>pg_rowsecurity</structname></title>
+
+ <indexterm zone="catalog-pg-rowsecurity">
+ <primary>pg_rowsecurity</primary>
+ </indexterm>
+ <para>
+ The catalog <structname>pg_rowsecurity</structname> stores expression
+ tree of row-security policy to be performed on a particular relation.
+ </para>
+ <table>
+ <title><structname>pg_rowsecurity</structname> Columns</title>
+ <tgroup cols="4">
+ <thead>
+ <row>
+ <entry>Name</entry>
+ <entry>Type</entry>
+ <entry>References</entry>
+ <entry>Description</entry>
+ </row>
+ </thead>
+ <tbody>
+ <row>
+ <entry><structfield>rsecrelid</structfield></entry>
+ <entry><type>oid</type></entry>
+ <entry><literal><link linkend="catalog-pg-class"><structname>pg_class</structname></link>.oid</literal></entry>
+ <entry>The table this row-security is for</entry>
+ </row>
+ <row>
+ <entry><structfield>rseccmd</structfield></entry>
+ <entry><type>char</type></entry>
+ <entry></entry>
+ <entry>The command this row-security is for. 'a' meaning all is only possible value right now.</entry>
+ </row>
+ <row>
+ <entry><structfield>rsecqual</structfield></entry>
+ <entry><type>pg_node_tree</type></entry>
+ <entry></entry>
+ <entry>An expression tree to be performed as row-security policy</entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+ <note>
+ <para>
+ <literal>pg_class.relhasrowsecurity</literal>
+ must be true if a table has row-security policy in this catalog.
+ </para>
+ </note>
+ </sect1>
<sect1 id="catalog-pg-seclabel">
<title><structname>pg_seclabel</structname></title>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
new file mode 100644
index 89649a2..79f37d8
*** a/doc/src/sgml/ref/alter_table.sgml
--- b/doc/src/sgml/ref/alter_table.sgml
*************** ALTER TABLE [ IF EXISTS ] <replaceable c
*** 69,74 ****
--- 69,76 ----
NOT OF
OWNER TO <replaceable class="PARAMETER">new_owner</replaceable>
SET TABLESPACE <replaceable class="PARAMETER">new_tablespace</replaceable>
+ SET ROW SECURITY FOR <replaceable class="PARAMETER">rowsec_command</replaceable> TO (<replaceable class="PARAMETER">condition</replaceable>)
+ RESET ROW SECURITY FOR <replaceable class="PARAMETER">rowsec_command</replaceable>
REPLICA IDENTITY {DEFAULT | USING INDEX <replaceable class="PARAMETER">index_name</replaceable> | FULL | NOTHING}
<phrase>and <replaceable class="PARAMETER">table_constraint_using_index</replaceable> is:</phrase>
*************** ALTER TABLE [ IF EXISTS ] <replaceable c
*** 76,81 ****
--- 78,85 ----
[ CONSTRAINT <replaceable class="PARAMETER">constraint_name</replaceable> ]
{ UNIQUE | PRIMARY KEY } USING INDEX <replaceable class="PARAMETER">index_name</replaceable>
[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+ <phrase>and <replaceable class="PARAMETER">rowsec_command</replaceable> is:</phrase>
+ { ALL | SELECT | INSERT | UPDATE | DELETE }
</synopsis>
</refsynopsisdiv>
*************** ALTER TABLE [ IF EXISTS ] <replaceable c
*** 581,586 ****
--- 585,610 ----
</varlistentry>
<varlistentry>
+ <term><literal>SET ROW SECURITY FOR <replaceable class="PARAMETER">rowsec_command</replaceable> TO (<replaceable class="PARAMETER">condition</replaceable>)</literal></term>
+ <listitem>
+ <para>
+ This form set row-level security policy of the table.
+ Supplied <replaceable class="PARAMETER">condition</replaceable> performs
+ as if it is implicitly appended to the qualifiers of <literal>WHERE</literal>
+ clause, although mechanism guarantees to evaluate this condition earlier
+ than any other user given condition.
+ <literal>ALL</> is the only supported command type right now.
+ See also <xref linkend="ROW-SECURITY">.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>RESET ROW SECURITY FOR <replaceable class="PARAMETER">rowsec_command</replaceable></literal></term>
+ <listitem>
+ <para>
+ This form reset row-level security policy of the table, if exists.
+ <literal>ALL</> is the only supported command type right now.
<term><literal>REPLICA IDENTITY</literal></term>
<listitem>
<para>
*************** ALTER TABLE [ IF EXISTS ] <replaceable c
*** 840,845 ****
--- 864,883 ----
</listitem>
</varlistentry>
+ <varlistentry>
+ <term><replaceable class="PARAMETER">condition</replaceable></term>
+ <listitem>
+ <para>
+ An expression that returns a value of type boolean. Expect for a case
+ when queries are executed with superuser privilege, only rows for which
+ this expression returns true will be fetched, updated or deleted.
+ This expression can reference columns of the relation being configured.
+ Sub-queries can be contained within expression tree, unless referenced
+ relation recursively references the same relation.
+ </para>
+ </listitem>
+ </varlistentry>
+
</variablelist>
</refsect1>
diff --git a/doc/src/sgml/user-manag.sgml b/doc/src/sgml/user-manag.sgml
new file mode 100644
index 177ac7a..c764189
*** a/doc/src/sgml/user-manag.sgml
--- b/doc/src/sgml/user-manag.sgml
*************** DROP ROLE <replaceable>name</replaceable
*** 439,442 ****
--- 439,587 ----
</para>
</sect1>
+ <sect1 id="row-security">
+ <title>Row-security</title>
+ <para>
+ <productname>PostgreSQL</productname> v9.4 or later provides
+ row-security feature, like several commercial database
+ management system. It allows table owner to assign a particular
+ condition that performs as a security policy of the table; only
+ rows that satisfies the condition should be visible, except for
+ a case when superuser runs queries.
+ </para>
+ <para>
+ Row-security policy can be set using <literal>SET ROW SECURITY</literal>
+ command of <xref linkend="SQL-ALTERTABLE"> 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.
+ </para>
+ <para>
+ For example, the following <literal>customer</literal> table has
+ <literal>uname</literal> field to store user name, and it assume
+ we don't want to expose any properties of other customers.
+ The following command set <literal>current_user = uname</literal>
+ as row-security policy on the <literal>customer</literal> table.
+ <screen>
+ postgres=> ALTER TABLE customer SET ROW SECURITY
+ FOR ALL TO (current_user = uname);
+ ALTER TABLE
+ </screen>
+ <xref linkend="SQL-EXPLAIN"> command shows how row-security policy
+ works on the supplied query.
+ <screen>
+ postgres=> EXPLAIN(costs off) SELECT * FROM customer WHERE f_leak(upasswd);
+ QUERY PLAN
+ --------------------------------------------
+ Subquery Scan on customer
+ Filter: f_leak(customer.upasswd)
+ -> Seq Scan on customer customer_1
+ Filter: ("current_user"() = uname)
+ (4 rows)
+ </screen>
+ This query execution plan means the preconfigured row-security policy is
+ implicitly added, and scan plan on the target relation being wrapped up
+ with a sub-query.
+ It ensures user given qualifiers, including functions with side effects,
+ are never executed earlier than the row-security policy regardless of
+ its cost, except for the cases when these were fully leakproof.
+ This design helps to tackle the scenario described in
+ <xref linkend="RULES-PRIVILEGES">; that introduces the order to evaluate
+ qualifiers is significant to keep confidentiality of invisible rows.
+ </para>
+
+ <para>
+ On the other hand, this design allows superusers to bypass checks with
+ row-security.
+ It ensures <application>pg_dump</application> can obtain a complete set
+ of database backup, and avoid to execute Trojan horse trap, being injected
+ as a row-security policy of user-defined table, with privileges of
+ superuser.
+ </para>
+
+ <para>
+ In case of queries on inherited tables, row-security policy of the parent
+ relation is not applied to child relations. Scope of the row-security
+ policy is limited to the relation on which it is set.
+ <screen>
+ postgres=> EXPLAIN(costs off) SELECT * FROM t1 WHERE f_leak(y);
+ QUERY PLAN
+ -------------------------------------
+ Append
+ -> Subquery Scan on t1
+ Filter: f_leak(t1.y)
+ -> Seq Scan on t1 t1_1
+ Filter: ((x % 2) = 0)
+ -> Seq Scan on t2
+ Filter: f_leak(y)
+ -> Subquery Scan on t3
+ Filter: f_leak(t3.y)
+ -> Seq Scan on t3 t3_1
+ Filter: ((x % 2) = 1)
+ (11 rows)
+ </screen>
+ In the above example, <literal>t1</literal> has inherited
+ child table <literal>t2</literal> and <literal>t3</literal>,
+ and row-security policy is set on only <literal>t1</literal>,
+ and <literal>t3</literal>, not <literal>t2</literal>.
+
+ The row-security policy of <literal>t1</literal>, <literal>x</literal>
+ must be even-number, is appended only <literal>t1</literal>,
+ neither <literal>t2</literal> nor <literal>t3</literal>.
+ On the contrary, <literal>t3</literal> has different row-security policy;
+ <literal>x</literal> must be odd-number.
+ </para>
+
+ <para>
+ Row-security feature also works to queries for writer-operations; such as
+ <xref linkend="SQL-INSERT">, <xref linkend="SQL-UPDATE"> or
+ <xref linkend="SQL-DELETE"> commands.
+ It ensures all the modified rows satisfies configured row-security policy.
+ The below query tries to update e-mail address of the
+ <literal>customer</> table, and configured row-security makes sure all the
+ modified rows's <literal>uname</> field has to match with
+ <literal>current_user</>.
+ </para>
+ <screen>
+ postgres=> EXPLAIN (costs off) UPDATE customer SET email = 'alice@example.com';
+ QUERY PLAN
+ --------------------------------------------------
+ Update on customer
+ -> Subquery Scan on customer
+ -> Seq Scan on customer customer_1
+ Filter: ("current_user"() = uname)
+ (4 rows)
+ </screen>
+ <para>
+ Modification of a certain table is consist of two different stuffs; one
+ is fetch rows to be modified from the result relation (except for
+ <literal>INSERT</> command), second is insertion of rows to the
+ result relation. Usually, before-row trigger or check constraints are
+ run in the second phase. In addition, row-security policy is also checked
+ on this stage, to prevent to insert or update rows with unprivileged
+ values.
+ </para>
+
+ <para>
+ Unlike other commercial database systems, we don't have any plan to allow
+ individual row-security policy for each command type. Even if we want to
+ perform with difference policy between <xref linkend="SQL-SELECT"> and
+ <xref linkend="SQL-DELETE">, <literal>RETURNING</> clause can leak the rows
+ to be invisible using <xref linkend="SQL-DELETE"> command.
+ </para>
+
+ <para>
+ Even though it is not a specific matter in row-security, please be careful
+ if you plan to use <literal>current_user</> in row-level security policy.
+ <xref linkend="SQL-DECLARE"> and <xref linkend="SQL-FETCH"> allows to
+ switch current user identifier during execution of the query.
+ Thus, it can fetch the first 100 rows with privilege of <literal>alice</>,
+ then remaining rows with privilege of <literal>bob</>. If and when query
+ execution plan contains some kind of materialization and row-security
+ policy contains <literal>current_user</>, the fetched tuples in
+ <literal>bob</>'s screen might be evaluated according to the privilege of
+ <literal>alice</>.
+ </para>
+ </sect1>
</chapter>
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
new file mode 100644
index a974bd5..beb73df
*** a/src/backend/catalog/Makefile
--- b/src/backend/catalog/Makefile
*************** OBJS = catalog.o dependency.o heap.o ind
*** 15,21 ****
pg_constraint.o pg_conversion.o \
pg_depend.o pg_enum.o pg_inherits.o pg_largeobject.o pg_namespace.o \
pg_operator.o pg_proc.o pg_range.o pg_db_role_setting.o pg_shdepend.o \
! pg_type.o storage.o toasting.o
BKIFILES = postgres.bki postgres.description postgres.shdescription
--- 15,21 ----
pg_constraint.o pg_conversion.o \
pg_depend.o pg_enum.o pg_inherits.o pg_largeobject.o pg_namespace.o \
pg_operator.o pg_proc.o pg_range.o pg_db_role_setting.o pg_shdepend.o \
! pg_rowsecurity.o pg_type.o storage.o toasting.o
BKIFILES = postgres.bki postgres.description postgres.shdescription
*************** POSTGRES_BKI_SRCS = $(addprefix $(top_sr
*** 39,45 ****
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_default_acl.h pg_seclabel.h pg_shseclabel.h pg_collation.h pg_range.h \
toasting.h indexing.h \
)
--- 39,45 ----
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_rowsecurity.h \
pg_default_acl.h pg_seclabel.h pg_shseclabel.h pg_collation.h pg_range.h \
toasting.h indexing.h \
)
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
new file mode 100644
index 908126c..3590ec5
*** a/src/backend/catalog/dependency.c
--- b/src/backend/catalog/dependency.c
***************
*** 45,50 ****
--- 45,51 ----
#include "catalog/pg_opfamily.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_rewrite.h"
+ #include "catalog/pg_rowsecurity.h"
#include "catalog/pg_tablespace.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_ts_config.h"
*************** doDeletion(const ObjectAddress *object,
*** 1249,1254 ****
--- 1250,1259 ----
RemoveEventTriggerById(object->objectId);
break;
+ case OCLASS_ROWSECURITY:
+ RemoveRowSecurityById(object->objectId);
+ break;
+
default:
elog(ERROR, "unrecognized object class: %u",
object->classId);
*************** getObjectClass(const ObjectAddress *obje
*** 2316,2321 ****
--- 2321,2329 ----
case EventTriggerRelationId:
return OCLASS_EVENT_TRIGGER;
+
+ case RowSecurityRelationId:
+ return OCLASS_ROWSECURITY;
}
/* shouldn't get here */
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
new file mode 100644
index 6f2e142..1ad6955
*** a/src/backend/catalog/heap.c
--- b/src/backend/catalog/heap.c
*************** InsertPgClassTuple(Relation pg_class_des
*** 798,803 ****
--- 798,804 ----
values[Anum_pg_class_relhaspkey - 1] = BoolGetDatum(rd_rel->relhaspkey);
values[Anum_pg_class_relhasrules - 1] = BoolGetDatum(rd_rel->relhasrules);
values[Anum_pg_class_relhastriggers - 1] = BoolGetDatum(rd_rel->relhastriggers);
+ values[Anum_pg_class_relhasrowsecurity - 1] = BoolGetDatum(rd_rel->relhasrowsecurity);
values[Anum_pg_class_relhassubclass - 1] = BoolGetDatum(rd_rel->relhassubclass);
values[Anum_pg_class_relispopulated - 1] = BoolGetDatum(rd_rel->relispopulated);
values[Anum_pg_class_relreplident - 1] = CharGetDatum(rd_rel->relreplident);
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
new file mode 100644
index 9011190..e66c91b
*** a/src/backend/catalog/objectaddress.c
--- b/src/backend/catalog/objectaddress.c
*************** getObjectDescription(const ObjectAddress
*** 2143,2148 ****
--- 2143,2198 ----
break;
}
+ case OCLASS_ROWSECURITY:
+ {
+ Relation rsec_rel;
+ ScanKeyData skey;
+ SysScanDesc sscan;
+ HeapTuple tuple;
+ Form_pg_rowsecurity form_rsec;
+
+ rsec_rel = heap_open(RowSecurityRelationId, AccessShareLock);
+
+ ScanKeyInit(&skey,
+ ObjectIdAttributeNumber,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(object->objectId));
+ sscan = systable_beginscan(rsec_rel, RowSecurityOidIndexId,
+ true, NULL, 1, &skey);
+ tuple = systable_getnext(sscan);
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "could not find tuple for row-security %u",
+ object->objectId);
+ form_rsec = (Form_pg_rowsecurity) GETSTRUCT(tuple);
+
+ appendStringInfo(&buffer, _("row-security of "));
+ getRelationDescription(&buffer, form_rsec->rsecrelid);
+ switch (form_rsec->rseccmd)
+ {
+ case ROWSECURITY_CMD_ALL:
+ appendStringInfo(&buffer, _(" FOR ALL"));
+ break;
+ case ROWSECURITY_CMD_SELECT:
+ appendStringInfo(&buffer, _(" FOR SELECT"));
+ break;
+ case ROWSECURITY_CMD_INSERT:
+ appendStringInfo(&buffer, _(" FOR INSERT"));
+ break;
+ case ROWSECURITY_CMD_UPDATE:
+ appendStringInfo(&buffer, _(" FOR UPDATE"));
+ break;
+ case ROWSECURITY_CMD_DELETE:
+ appendStringInfo(&buffer, _(" FOR DELETE"));
+ break;
+ default:
+ elog(ERROR, "unexpected row-security command type: %c",
+ form_rsec->rseccmd);
+ }
+ systable_endscan(sscan);
+ heap_close(rsec_rel, AccessShareLock);
+ break;
+ }
+
default:
appendStringInfo(&buffer, "unrecognized object %u %u %d",
object->classId,
diff --git a/src/backend/catalog/pg_rowsecurity.c b/src/backend/catalog/pg_rowsecurity.c
new file mode 100644
index ...34d33e8
*** a/src/backend/catalog/pg_rowsecurity.c
--- b/src/backend/catalog/pg_rowsecurity.c
***************
*** 0 ****
--- 1,337 ----
+ /* -------------------------------------------------------------------------
+ *
+ * pg_rowsecurity.c
+ * routines to support manipulation of the pg_rowsecurity catalog
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * -------------------------------------------------------------------------
+ */
+ #include "postgres.h"
+ #include "access/genam.h"
+ #include "access/heapam.h"
+ #include "access/htup_details.h"
+ #include "access/sysattr.h"
+ #include "catalog/dependency.h"
+ #include "catalog/indexing.h"
+ #include "catalog/pg_class.h"
+ #include "catalog/pg_rowsecurity.h"
+ #include "catalog/pg_type.h"
+ #include "nodes/nodeFuncs.h"
+ #include "optimizer/clauses.h"
+ #include "parser/parse_clause.h"
+ #include "parser/parse_node.h"
+ #include "parser/parse_relation.h"
+ #include "utils/builtins.h"
+ #include "utils/fmgroids.h"
+ #include "utils/inval.h"
+ #include "utils/rel.h"
+ #include "utils/syscache.h"
+ #include "utils/tqual.h"
+
+ /*
+ * Load row-security policy from the catalog, and keep it on
+ * the relation cache.
+ */
+ void
+ RelationBuildRowSecurity(Relation relation)
+ {
+ Relation catalog;
+ ScanKeyData skey;
+ SysScanDesc sscan;
+ HeapTuple tuple;
+ MemoryContext oldcxt;
+ MemoryContext rscxt = NULL;
+ RowSecurityDesc *rsdesc = NULL;
+
+ catalog = heap_open(RowSecurityRelationId, AccessShareLock);
+
+ ScanKeyInit(&skey,
+ Anum_pg_rowsecurity_rsecrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(RelationGetRelid(relation)));
+ sscan = systable_beginscan(catalog, RowSecurityRelidIndexId, true,
+ NULL, 1, &skey);
+ PG_TRY();
+ {
+ while (HeapTupleIsValid(tuple = systable_getnext(sscan)))
+ {
+ Datum value;
+ bool isnull;
+ char *temp;
+
+ if (!rsdesc)
+ {
+ rscxt = AllocSetContextCreate(CacheMemoryContext,
+ "Row-security descriptor",
+ ALLOCSET_SMALL_MINSIZE,
+ ALLOCSET_SMALL_INITSIZE,
+ ALLOCSET_SMALL_MAXSIZE);
+ oldcxt = MemoryContextSwitchTo(rscxt);
+ rsdesc = palloc0(sizeof(RowSecurityDesc));
+ rsdesc->rscxt = rscxt;
+ MemoryContextSwitchTo(oldcxt);
+ }
+ value = heap_getattr(tuple, Anum_pg_rowsecurity_rseccmd,
+ RelationGetDescr(catalog), &isnull);
+ Assert(!isnull);
+
+ if (DatumGetChar(value) != ROWSECURITY_CMD_ALL)
+ {
+ ereport(WARNING,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("Per-command row-security not implemented")));
+ continue;
+ }
+
+ value = heap_getattr(tuple, Anum_pg_rowsecurity_rsecqual,
+ RelationGetDescr(catalog), &isnull);
+ Assert(!isnull);
+ temp = TextDatumGetCString(value);
+
+ oldcxt = MemoryContextSwitchTo(rscxt);
+ rsdesc->rsall.rsecid = HeapTupleGetOid(tuple);
+ rsdesc->rsall.qual = (Expr *) stringToNode(temp);
+ Assert(exprType((Node *)rsdesc->rsall.qual) == BOOLOID);
+ rsdesc->rsall.hassublinks
+ = contain_subplans((Node *)rsdesc->rsall.qual);
+ MemoryContextSwitchTo(oldcxt);
+
+ pfree(temp);
+ }
+ }
+ PG_CATCH();
+ {
+ if (rscxt != NULL)
+ MemoryContextDelete(rscxt);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ systable_endscan(sscan);
+ heap_close(catalog, AccessShareLock);
+
+ relation->rsdesc = rsdesc;
+ }
+
+ /*
+ * Parse the supplied row-security policy, and insert/update a row
+ * of pg_rowsecurity catalog.
+ */
+ static void
+ InsertOrUpdatePolicyRow(Relation relation, char rseccmd, Node *clause)
+ {
+ Oid relationId = RelationGetRelid(relation);
+ Oid rowsecId;
+ ParseState *pstate;
+ RangeTblEntry *rte;
+ Node *qual;
+ Relation catalog;
+ ScanKeyData skeys[2];
+ SysScanDesc sscan;
+ HeapTuple oldtup;
+ HeapTuple newtup;
+ Datum values[Natts_pg_rowsecurity];
+ bool isnull[Natts_pg_rowsecurity];
+ bool replaces[Natts_pg_rowsecurity];
+ ObjectAddress target;
+ ObjectAddress myself;
+
+ /* Parse the supplied clause */
+ pstate = make_parsestate(NULL);
+
+ rte = addRangeTableEntryForRelation(pstate, relation,
+ NULL, false, false);
+ addRTEtoQuery(pstate, rte, false, true, true);
+
+ qual = transformWhereClause(pstate, copyObject(clause),
+ EXPR_KIND_ROW_SECURITY,
+ "ROW SECURITY");
+ /* zero-clear */
+ memset(values, 0, sizeof(values));
+ memset(replaces, 0, sizeof(replaces));
+ memset(isnull, 0, sizeof(isnull));
+
+ /* Update or Insert an entry to pg_rowsecurity catalog */
+ catalog = heap_open(RowSecurityRelationId, RowExclusiveLock);
+
+ ScanKeyInit(&skeys[0],
+ Anum_pg_rowsecurity_rsecrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(RelationGetRelid(relation)));
+ ScanKeyInit(&skeys[1],
+ Anum_pg_rowsecurity_rseccmd,
+ BTEqualStrategyNumber, F_CHAREQ,
+ CharGetDatum(rseccmd));
+ sscan = systable_beginscan(catalog, RowSecurityRelidIndexId, true,
+ NULL, 2, skeys);
+ oldtup = systable_getnext(sscan);
+ if (HeapTupleIsValid(oldtup))
+ {
+ rowsecId = HeapTupleGetOid(oldtup);
+
+ replaces[Anum_pg_rowsecurity_rsecqual - 1] = true;
+ values[Anum_pg_rowsecurity_rsecqual - 1]
+ = CStringGetTextDatum(nodeToString(qual));
+
+ newtup = heap_modify_tuple(oldtup,
+ RelationGetDescr(catalog),
+ values, isnull, replaces);
+ simple_heap_update(catalog, &newtup->t_self, newtup);
+
+ deleteDependencyRecordsFor(RowSecurityRelationId, rowsecId, false);
+ }
+ else
+ {
+ values[Anum_pg_rowsecurity_rsecrelid - 1]
+ = ObjectIdGetDatum(relationId);
+ values[Anum_pg_rowsecurity_rseccmd - 1]
+ = CharGetDatum(rseccmd);
+ values[Anum_pg_rowsecurity_rsecqual - 1]
+ = CStringGetTextDatum(nodeToString(qual));
+ newtup = heap_form_tuple(RelationGetDescr(catalog),
+ values, isnull);
+ rowsecId = simple_heap_insert(catalog, newtup);
+ }
+ CatalogUpdateIndexes(catalog, newtup);
+
+ heap_freetuple(newtup);
+
+ /* records dependencies of row-security policy and relation/columns */
+ target.classId = RelationRelationId;
+ target.objectId = relationId;
+ target.objectSubId = 0;
+
+ myself.classId = RowSecurityRelationId;
+ myself.objectId = rowsecId;
+ myself.objectSubId = 0;
+
+ recordDependencyOn(&myself, &target, DEPENDENCY_AUTO);
+
+ recordDependencyOnExpr(&myself, qual, pstate->p_rtable,
+ DEPENDENCY_NORMAL);
+ free_parsestate(pstate);
+
+ systable_endscan(sscan);
+ heap_close(catalog, RowExclusiveLock);
+ }
+
+ /*
+ * Remove row-security policy row of pg_rowsecurity
+ */
+ static void
+ DeletePolicyRow(Relation relation, char rseccmd)
+ {
+ Assert(rseccmd == ROWSECURITY_CMD_ALL);
+
+ if (relation->rsdesc)
+ {
+ ObjectAddress address;
+
+ address.classId = RowSecurityRelationId;
+ address.objectId = relation->rsdesc->rsall.rsecid;
+ address.objectSubId = 0;
+
+ performDeletion(&address, DROP_RESTRICT, 0);
+ }
+ else
+ {
+ /* Nothing to do here */
+ elog(INFO, "relation %s has no row-security policy, skipped",
+ RelationGetRelationName(relation));
+ }
+ }
+
+ /*
+ * Guts of row-security policy deletion.
+ */
+ void
+ RemoveRowSecurityById(Oid rowsecId)
+ {
+ Relation catalog;
+ ScanKeyData skey;
+ SysScanDesc sscan;
+ HeapTuple tuple;
+ Relation rel;
+ Oid relid;
+
+ catalog = heap_open(RowSecurityRelationId, RowExclusiveLock);
+
+ ScanKeyInit(&skey,
+ ObjectIdAttributeNumber,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(rowsecId));
+ sscan = systable_beginscan(catalog, RowSecurityOidIndexId, true,
+ NULL, 1, &skey);
+ tuple = systable_getnext(sscan);
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "could not find tuple for row-security %u", rowsecId);
+
+ /*
+ * Open and exclusive-lock the relation the row-security belongs to.
+ */
+ relid = ((Form_pg_rowsecurity) GETSTRUCT(tuple))->rsecrelid;
+
+ rel = heap_open(relid, AccessExclusiveLock);
+
+ simple_heap_delete(catalog, &tuple->t_self);
+
+ /* Ensure relcache entries of other session being rebuilt */
+ CacheInvalidateRelcache(rel);
+
+ heap_close(rel, NoLock);
+
+ systable_endscan(sscan);
+ heap_close(catalog, RowExclusiveLock);
+ }
+
+ /*
+ * ALTER TABLE <name> SET ROW SECURITY (...) OR
+ * RESET ROW SECURITY
+ */
+ void
+ ATExecSetRowSecurity(Relation relation, const char *cmdname, Node *clause)
+ {
+ Oid relid = RelationGetRelid(relation);
+ char rseccmd;
+
+ if (strcmp(cmdname, "all") == 0)
+ rseccmd = ROWSECURITY_CMD_ALL;
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("Row-security for \"%s\" is not implemented yet",
+ cmdname)));
+
+ if (clause != NULL)
+ {
+ InsertOrUpdatePolicyRow(relation, rseccmd, clause);
+
+ /*
+ * Also, turn on relhasrowsecurity, if not.
+ */
+ if (!RelationGetForm(relation)->relhasrowsecurity)
+ {
+ Relation class_rel = heap_open(RelationRelationId,
+ RowExclusiveLock);
+ HeapTuple tuple;
+
+ tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for relation %u", relid);
+
+ ((Form_pg_class) GETSTRUCT(tuple))->relhasrowsecurity = true;
+
+ simple_heap_update(class_rel, &tuple->t_self, tuple);
+ CatalogUpdateIndexes(class_rel, tuple);
+
+ heap_freetuple(tuple);
+ heap_close(class_rel, RowExclusiveLock);
+ }
+ }
+ else
+ DeletePolicyRow(relation, rseccmd);
+
+ CacheInvalidateRelcache(relation);
+ }
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
new file mode 100644
index 0fa83a6..d657931
*** a/src/backend/commands/copy.c
--- b/src/backend/commands/copy.c
***************
*** 24,29 ****
--- 24,30 ----
#include "access/htup_details.h"
#include "access/sysattr.h"
#include "access/xact.h"
+ #include "catalog/heap.h"
#include "catalog/namespace.h"
#include "catalog/pg_type.h"
#include "commands/copy.h"
***************
*** 34,48 ****
--- 35,53 ----
#include "libpq/pqformat.h"
#include "mb/pg_wchar.h"
#include "miscadmin.h"
+ #include "nodes/makefuncs.h"
#include "optimizer/clauses.h"
#include "optimizer/planner.h"
+ #include "optimizer/rowsecurity.h"
#include "parser/parse_relation.h"
+ #include "parser/parsetree.h"
#include "rewrite/rewriteHandler.h"
#include "storage/fd.h"
#include "tcop/tcopprot.h"
#include "utils/acl.h"
#include "utils/builtins.h"
#include "utils/lsyscache.h"
+ #include "utils/syscache.h"
#include "utils/memutils.h"
#include "utils/portal.h"
#include "utils/rel.h"
*************** DoCopy(const CopyStmt *stmt, const char
*** 814,819 ****
--- 819,839 ----
rel = heap_openrv(stmt->relation,
(is_from ? RowExclusiveLock : AccessShareLock));
+ tupDesc = RelationGetDescr(rel);
+ attnums = CopyGetAttnums(tupDesc, rel, stmt->attlist);
+
+ /*
+ * We have to run regular query, if the target relation has
+ * row-level security policy
+ */
+ if (copy_row_security_policy((CopyStmt *)stmt, rel, attnums))
+ {
+ heap_close(rel, NoLock); /* close with keeping lock */
+ relid = InvalidOid;
+ rel = NULL;
+ }
+ else
+ {
relid = RelationGetRelid(rel);
rte = makeNode(RangeTblEntry);
*************** DoCopy(const CopyStmt *stmt, const char
*** 822,829 ****
rte->relkind = rel->rd_rel->relkind;
rte->requiredPerms = required_access;
- tupDesc = RelationGetDescr(rel);
- attnums = CopyGetAttnums(tupDesc, rel, stmt->attlist);
foreach(cur, attnums)
{
int attno = lfirst_int(cur) -
--- 842,847 ----
*************** DoCopy(const CopyStmt *stmt, const char
*** 835,840 ****
--- 853,859 ----
rte->selectedCols = bms_add_member(rte->selectedCols, attno);
}
ExecCheckRTPerms(list_make1(rte), true);
+ }
}
else
{
*************** ProcessCopyOptions(CopyState cstate,
*** 1193,1198 ****
--- 1212,1264 ----
}
/*
+ * Adjust Query tree constructed with row-level security feature.
+ * If WITH OIDS option was supplied, it adds Var node to reference
+ * object-id system column.
+ */
+ static void
+ fixup_oid_of_rls_query(Query *query)
+ {
+ RangeTblEntry *subrte;
+ TargetEntry *subtle;
+ Var *subvar;
+ ListCell *cell;
+ Form_pg_attribute attform
+ = SystemAttributeDefinition(ObjectIdAttributeNumber, true);
+
+ subrte = rt_fetch((Index) 1, query->rtable);
+ Assert(subrte->rtekind == RTE_RELATION);
+
+ if (!SearchSysCacheExists2(ATTNUM,
+ ObjectIdGetDatum(subrte->relid),
+ Int16GetDatum(attform->attnum)))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_COLUMN),
+ errmsg("table \"%s\" does not have OIDs",
+ get_rel_name(subrte->relid))));
+
+ subvar = makeVar((Index) 1,
+ attform->attnum,
+ attform->atttypid,
+ attform->atttypmod,
+ attform->attcollation,
+ 0);
+ subtle = makeTargetEntry((Expr *) subvar,
+ 0,
+ pstrdup(NameStr(attform->attname)),
+ false);
+
+ query->targetList = list_concat(list_make1(subtle),
+ query->targetList);
+ /* adjust resno of TargetEntry */
+ foreach (cell, query->targetList)
+ {
+ subtle = lfirst(cell);
+ subtle->resno++;
+ }
+ }
+
+ /*
* Common setup routines used by BeginCopyFrom and BeginCopyTo.
*
* Iff <binary>, unload or reload in the binary format, as opposed to the
*************** BeginCopy(bool is_from,
*** 1264,1269 ****
--- 1330,1354 ----
Assert(!is_from);
cstate->rel = NULL;
+ /*
+ * In case when regular COPY TO was replaced because of row-level
+ * security, "raw_query" node have already analyzed / rewritten
+ * query tree.
+ */
+ if (IsA(raw_query, Query))
+ {
+ query = (Query *) raw_query;
+
+ Assert(query->querySource == QSRC_ROW_SECURITY);
+ if (cstate->oids)
+ {
+ fixup_oid_of_rls_query(query);
+ cstate->oids = false;
+ }
+ attnamelist = NIL;
+ }
+ else
+ {
/* Don't allow COPY w/ OIDs from a select */
if (cstate->oids)
ereport(ERROR,
*************** BeginCopy(bool is_from,
*** 1288,1293 ****
--- 1373,1379 ----
elog(ERROR, "unexpected rewrite result");
query = (Query *) linitial(rewritten);
+ }
/* The grammar allows SELECT INTO, but we don't support that */
if (query->utilityStmt != NULL &&
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
new file mode 100644
index 328e2a8..0374629
*** a/src/backend/commands/event_trigger.c
--- b/src/backend/commands/event_trigger.c
*************** EventTriggerSupportsObjectClass(ObjectCl
*** 992,997 ****
--- 992,998 ----
case OCLASS_USER_MAPPING:
case OCLASS_DEFACL:
case OCLASS_EXTENSION:
+ case OCLASS_ROWSECURITY:
return true;
case MAX_OCLASS:
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
new file mode 100644
index 1d9f29a..afe77c7
*** a/src/backend/commands/tablecmds.c
--- b/src/backend/commands/tablecmds.c
***************
*** 37,42 ****
--- 37,43 ----
#include "catalog/pg_inherits_fn.h"
#include "catalog/pg_namespace.h"
#include "catalog/pg_opclass.h"
+ #include "catalog/pg_rowsecurity.h"
#include "catalog/pg_tablespace.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
*************** AlterTableGetLockLevel(List *cmds)
*** 2789,2794 ****
--- 2790,2797 ----
case AT_SetTableSpace: /* must rewrite heap */
case AT_DropNotNull: /* may change some SQL plans */
case AT_SetNotNull:
+ case AT_SetRowSecurity:
+ case AT_ResetRowSecurity:
case AT_GenericOptions:
case AT_AlterColumnGenericOptions:
cmd_lockmode = AccessExclusiveLock;
*************** ATPrepCmd(List **wqueue, Relation rel, A
*** 3164,3169 ****
--- 3167,3174 ----
case AT_DropInherit: /* NO INHERIT */
case AT_AddOf: /* OF */
case AT_DropOf: /* NOT OF */
+ case AT_SetRowSecurity:
+ case AT_ResetRowSecurity:
ATSimplePermissions(rel, ATT_TABLE);
/* These commands never recurse */
/* No command-specific prep needed */
*************** ATExecCmd(List **wqueue, AlteredTableInf
*** 3449,3454 ****
--- 3454,3464 ----
case AT_DropOf:
ATExecDropOf(rel, lockmode);
break;
+ case AT_SetRowSecurity:
+ ATExecSetRowSecurity(rel, cmd->name, (Node *) cmd->def);
+ break;
+ case AT_ResetRowSecurity:
+ ATExecSetRowSecurity(rel, cmd->name, NULL);
case AT_ReplicaIdentity:
ATExecReplicaIdentity(rel, (ReplicaIdentityStmt *) cmd->def, lockmode);
break;
*************** ATExecAlterColumnType(AlteredTableInfo *
*** 7808,7813 ****
--- 7818,7839 ----
Assert(defaultexpr);
break;
+ case OCLASS_ROWSECURITY:
+ /*
+ * A row-level security policy can depend on a column in case
+ * when the policy clause references a particular column.
+ * Due to same reason why TRIGGER ... WHEN does not support
+ * to change column's type being referenced in clause, row-
+ * level security policy also does not support it.
+ */
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot alter type of a column used in a row-level security policy"),
+ errdetail("%s depends on column \"%s\"",
+ getObjectDescription(&foundObject),
+ colName)));
+ break;
+
case OCLASS_PROC:
case OCLASS_TYPE:
case OCLASS_CAST:
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
new file mode 100644
index 6be17a9..92772bc
*** a/src/backend/executor/execMain.c
--- b/src/backend/executor/execMain.c
*************** InitPlan(QueryDesc *queryDesc, int eflag
*** 789,796 ****
foreach(l, plannedstmt->rowMarks)
{
PlanRowMark *rc = (PlanRowMark *) lfirst(l);
! Oid relid;
! Relation relation;
ExecRowMark *erm;
/* ignore "parent" rowmarks; they are irrelevant at runtime */
--- 789,797 ----
foreach(l, plannedstmt->rowMarks)
{
PlanRowMark *rc = (PlanRowMark *) lfirst(l);
! RangeTblEntry *rte = NULL;
! Relation relation = NULL;
! LOCKMODE lockmode = NoLock;
ExecRowMark *erm;
/* ignore "parent" rowmarks; they are irrelevant at runtime */
*************** InitPlan(QueryDesc *queryDesc, int eflag
*** 803,829 ****
case ROW_MARK_NOKEYEXCLUSIVE:
case ROW_MARK_SHARE:
case ROW_MARK_KEYSHARE:
! relid = getrelid(rc->rti, rangeTable);
! relation = heap_open(relid, RowShareLock);
break;
case ROW_MARK_REFERENCE:
! relid = getrelid(rc->rti, rangeTable);
! relation = heap_open(relid, AccessShareLock);
break;
case ROW_MARK_COPY:
/* there's no real table here ... */
- relation = NULL;
break;
default:
elog(ERROR, "unrecognized markType: %d", rc->markType);
- relation = NULL; /* keep compiler quiet */
break;
}
/* Check that relation is a legal target for marking */
! if (relation)
CheckValidRowMarkRel(relation, rc->markType);
!
erm = (ExecRowMark *) palloc(sizeof(ExecRowMark));
erm->relation = relation;
erm->rti = rc->rti;
--- 804,836 ----
case ROW_MARK_NOKEYEXCLUSIVE:
case ROW_MARK_SHARE:
case ROW_MARK_KEYSHARE:
! rte = rt_fetch(rc->rti, rangeTable);
! lockmode = RowShareLock;
break;
case ROW_MARK_REFERENCE:
! rte = rt_fetch(rc->rti, rangeTable);
! lockmode = AccessShareLock;
break;
case ROW_MARK_COPY:
/* there's no real table here ... */
break;
default:
elog(ERROR, "unrecognized markType: %d", rc->markType);
break;
}
/* Check that relation is a legal target for marking */
! if (rte)
! {
! if (rte->rtekind == RTE_RELATION)
! relation = heap_open(rte->relid, lockmode);
! else
! {
! Assert(rte->rtekind == RTE_SUBQUERY);
! relation = heap_open(rte->rowsec_relid, lockmode);
! }
CheckValidRowMarkRel(relation, rc->markType);
! }
erm = (ExecRowMark *) palloc(sizeof(ExecRowMark));
erm->relation = relation;
erm->rti = rc->rti;
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
new file mode 100644
index e3edcf6..d5ce3d2
*** a/src/backend/nodes/copyfuncs.c
--- b/src/backend/nodes/copyfuncs.c
*************** _copyAppendRelInfo(const AppendRelInfo *
*** 1934,1939 ****
--- 1934,1940 ----
COPY_SCALAR_FIELD(parent_relid);
COPY_SCALAR_FIELD(child_relid);
+ COPY_SCALAR_FIELD(child_result);
COPY_SCALAR_FIELD(parent_reltype);
COPY_SCALAR_FIELD(child_reltype);
COPY_NODE_FIELD(translated_vars);
*************** _copyRangeTblEntry(const RangeTblEntry *
*** 1975,1980 ****
--- 1976,1982 ----
COPY_SCALAR_FIELD(relkind);
COPY_NODE_FIELD(subquery);
COPY_SCALAR_FIELD(security_barrier);
+ COPY_SCALAR_FIELD(rowsec_relid);
COPY_SCALAR_FIELD(jointype);
COPY_NODE_FIELD(joinaliasvars);
COPY_NODE_FIELD(functions);
*************** _copyQuery(const Query *from)
*** 2463,2468 ****
--- 2465,2471 ----
COPY_SCALAR_FIELD(canSetTag);
COPY_NODE_FIELD(utilityStmt);
COPY_SCALAR_FIELD(resultRelation);
+ COPY_SCALAR_FIELD(sourceRelation);
COPY_SCALAR_FIELD(hasAggs);
COPY_SCALAR_FIELD(hasWindowFuncs);
COPY_SCALAR_FIELD(hasSubLinks);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
new file mode 100644
index 1f9b5d7..082d3d4
*** a/src/backend/nodes/equalfuncs.c
--- b/src/backend/nodes/equalfuncs.c
*************** _equalAppendRelInfo(const AppendRelInfo
*** 812,817 ****
--- 812,818 ----
{
COMPARE_SCALAR_FIELD(parent_relid);
COMPARE_SCALAR_FIELD(child_relid);
+ COMPARE_SCALAR_FIELD(child_result);
COMPARE_SCALAR_FIELD(parent_reltype);
COMPARE_SCALAR_FIELD(child_reltype);
COMPARE_NODE_FIELD(translated_vars);
*************** _equalQuery(const Query *a, const Query
*** 847,852 ****
--- 848,854 ----
COMPARE_SCALAR_FIELD(canSetTag);
COMPARE_NODE_FIELD(utilityStmt);
COMPARE_SCALAR_FIELD(resultRelation);
+ COMPARE_SCALAR_FIELD(sourceRelation);
COMPARE_SCALAR_FIELD(hasAggs);
COMPARE_SCALAR_FIELD(hasWindowFuncs);
COMPARE_SCALAR_FIELD(hasSubLinks);
*************** _equalRangeTblEntry(const RangeTblEntry
*** 2245,2250 ****
--- 2247,2253 ----
COMPARE_SCALAR_FIELD(relkind);
COMPARE_NODE_FIELD(subquery);
COMPARE_SCALAR_FIELD(security_barrier);
+ COMPARE_SCALAR_FIELD(rowsec_relid);
COMPARE_SCALAR_FIELD(jointype);
COMPARE_NODE_FIELD(joinaliasvars);
COMPARE_NODE_FIELD(functions);
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
new file mode 100644
index d7db67d..b9b5aa2
*** a/src/backend/nodes/nodeFuncs.c
--- b/src/backend/nodes/nodeFuncs.c
*************** query_tree_walker(Query *query,
*** 1941,1948 ****
return true;
if (walker((Node *) query->withCheckOptions, context))
return true;
! if (walker((Node *) query->returningList, context))
! return true;
if (walker((Node *) query->jointree, context))
return true;
if (walker(query->setOperations, context))
--- 1941,1951 ----
return true;
if (walker((Node *) query->withCheckOptions, context))
return true;
! if (!(flags & QTW_IGNORE_RETURNING))
! {
! if (walker((Node *) query->returningList, context))
! return true;
! }
if (walker((Node *) query->jointree, context))
return true;
if (walker(query->setOperations, context))
*************** query_tree_mutator(Query *query,
*** 2678,2684 ****
MUTATE(query->targetList, query->targetList, List *);
MUTATE(query->withCheckOptions, query->withCheckOptions, List *);
! MUTATE(query->returningList, query->returningList, List *);
MUTATE(query->jointree, query->jointree, FromExpr *);
MUTATE(query->setOperations, query->setOperations, Node *);
MUTATE(query->havingQual, query->havingQual, Node *);
--- 2681,2690 ----
MUTATE(query->targetList, query->targetList, List *);
MUTATE(query->withCheckOptions, query->withCheckOptions, List *);
! if (!(flags & QTW_IGNORE_RETURNING))
! MUTATE(query->returningList, query->returningList, List *);
! else
! query->returningList = copyObject(query->returningList);
MUTATE(query->jointree, query->jointree, FromExpr *);
MUTATE(query->setOperations, query->setOperations, Node *);
MUTATE(query->havingQual, query->havingQual, Node *);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
new file mode 100644
index 4c7505e..fde3853
*** a/src/backend/nodes/outfuncs.c
--- b/src/backend/nodes/outfuncs.c
*************** _outAppendRelInfo(StringInfo str, const
*** 1923,1928 ****
--- 1923,1929 ----
WRITE_UINT_FIELD(parent_relid);
WRITE_UINT_FIELD(child_relid);
+ WRITE_UINT_FIELD(child_result);
WRITE_OID_FIELD(parent_reltype);
WRITE_OID_FIELD(child_reltype);
WRITE_NODE_FIELD(translated_vars);
*************** _outQuery(StringInfo str, const Query *n
*** 2238,2243 ****
--- 2239,2245 ----
appendStringInfoString(str, " :utilityStmt <>");
WRITE_INT_FIELD(resultRelation);
+ WRITE_INT_FIELD(sourceRelation);
WRITE_BOOL_FIELD(hasAggs);
WRITE_BOOL_FIELD(hasWindowFuncs);
WRITE_BOOL_FIELD(hasSubLinks);
*************** _outRangeTblEntry(StringInfo str, const
*** 2373,2378 ****
--- 2375,2381 ----
case RTE_SUBQUERY:
WRITE_NODE_FIELD(subquery);
WRITE_BOOL_FIELD(security_barrier);
+ WRITE_OID_FIELD(rowsec_relid);
break;
case RTE_JOIN:
WRITE_ENUM_FIELD(jointype, JoinType);
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
new file mode 100644
index 2e2cfa7..9f32519
*** a/src/backend/nodes/readfuncs.c
--- b/src/backend/nodes/readfuncs.c
*************** _readQuery(void)
*** 199,204 ****
--- 199,205 ----
READ_BOOL_FIELD(canSetTag);
READ_NODE_FIELD(utilityStmt);
READ_INT_FIELD(resultRelation);
+ READ_INT_FIELD(sourceRelation);
READ_BOOL_FIELD(hasAggs);
READ_BOOL_FIELD(hasWindowFuncs);
READ_BOOL_FIELD(hasSubLinks);
*************** _readRangeTblEntry(void)
*** 1214,1219 ****
--- 1215,1221 ----
case RTE_SUBQUERY:
READ_NODE_FIELD(subquery);
READ_BOOL_FIELD(security_barrier);
+ READ_OID_FIELD(rowsec_relid);
break;
case RTE_JOIN:
READ_ENUM_FIELD(jointype, JoinType);
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
new file mode 100644
index 6670794..8b171ce
*** a/src/backend/optimizer/plan/planner.c
--- b/src/backend/optimizer/plan/planner.c
***************
*** 33,38 ****
--- 33,39 ----
#include "optimizer/planmain.h"
#include "optimizer/planner.h"
#include "optimizer/prep.h"
+ #include "optimizer/rowsecurity.h"
#include "optimizer/subselect.h"
#include "optimizer/tlist.h"
#include "parser/analyze.h"
*************** standard_planner(Query *parse, int curso
*** 177,182 ****
--- 178,184 ----
glob->lastPHId = 0;
glob->lastRowMarkId = 0;
glob->transientPlan = false;
+ glob->planUserId = InvalidOid;
/* Determine what fraction of the plan is likely to be scanned */
if (cursorOptions & CURSOR_OPT_FAST_PLAN)
*************** standard_planner(Query *parse, int curso
*** 254,259 ****
--- 256,262 ----
result->relationOids = glob->relationOids;
result->invalItems = glob->invalItems;
result->nParamExec = glob->nParamExec;
+ result->planUserId = glob->planUserId;
return result;
}
*************** subquery_planner(PlannerGlobal *glob, Qu
*** 404,409 ****
--- 407,425 ----
expand_inherited_tables(root);
/*
+ * Apply row-security policy of the relation being referenced,
+ * if configured with either of built-in or extension's features.
+ * RangeTblEntry of the relation with row-security policy shall
+ * be replaced with a row-security subquery that has simple scan
+ * on the target relation with row-security policy qualifiers.
+ *
+ * This routine assumes PlannerInfo is already handled with
+ * expand_inherited_tables, thus, AppendRelInfo or PlanRowMark
+ * have valid information.
+ */
+ apply_row_security_policy(root);
+
+ /*
* Set hasHavingQual to remember if HAVING clause is present. Needed
* because preprocess_expression will reduce a constant-true condition to
* an empty qual list ... but "HAVING TRUE" is not a semantic no-op.
*************** inheritance_planner(PlannerInfo *root)
*** 888,893 ****
--- 904,911 ----
newrti = list_length(subroot.parse->rtable) + 1;
ChangeVarNodes((Node *) subroot.parse, rti, newrti, 0);
ChangeVarNodes((Node *) subroot.rowMarks, rti, newrti, 0);
+ if (subroot.parse->sourceRelation == rti)
+ subroot.parse->sourceRelation = newrti;
rte = copyObject(rte);
subroot.parse->rtable = lappend(subroot.parse->rtable,
rte);
*************** inheritance_planner(PlannerInfo *root)
*** 951,957 ****
root->init_plans = subroot.init_plans;
/* Build list of target-relation RT indexes */
! resultRelations = lappend_int(resultRelations, appinfo->child_relid);
/* Build lists of per-relation WCO and RETURNING targetlists */
if (parse->withCheckOptions)
--- 969,978 ----
root->init_plans = subroot.init_plans;
/* Build list of target-relation RT indexes */
! resultRelations = lappend_int(resultRelations,
! (appinfo->child_result > 0 ?
! appinfo->child_result :
! appinfo->child_relid));
/* Build lists of per-relation WCO and RETURNING targetlists */
if (parse->withCheckOptions)
diff --git a/src/backend/optimizer/prep/preptlist.c b/src/backend/optimizer/prep/preptlist.c
new file mode 100644
index fb67f9e..0cccdf5
*** a/src/backend/optimizer/prep/preptlist.c
--- b/src/backend/optimizer/prep/preptlist.c
***************
*** 37,44 ****
static List *expand_targetlist(List *tlist, int command_type,
! Index result_relation, List *range_table);
/*
* preprocess_targetlist
--- 37,85 ----
static List *expand_targetlist(List *tlist, int command_type,
! Index result_relation, Index source_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 Var that references sub-queries being originated from regular
! * relations with row-level security policy due to nature of sub-query
! * that has no system-column.
! */
! static AttrNumber
! lookup_varattno(AttrNumber attno, Index rt_index, List *rtables)
! {
! RangeTblEntry *rte = rt_fetch(rt_index, rtables);
!
! if (rte->rtekind == RTE_SUBQUERY &&
! rte->subquery->querySource == QSRC_ROW_SECURITY)
! {
! ListCell *cell;
!
! foreach (cell, rte->subquery->targetList)
! {
! TargetEntry *tle = lfirst(cell);
! Var *var;
!
! if (IsA(tle->expr, Const))
! continue;
!
! var = (Var *) tle->expr;
! Assert(IsA(var, Var));
+ if (var->varattno == attno)
+ return tle->resno;
+ }
+ elog(ERROR, "invalid attno %d on row-security subquery target-list",
+ attno);
+ }
+ return attno;
+ }
/*
* preprocess_targetlist
*************** preprocess_targetlist(PlannerInfo *root,
*** 51,56 ****
--- 92,98 ----
{
Query *parse = root->parse;
int result_relation = parse->resultRelation;
+ int source_relation = parse->sourceRelation;
List *range_table = parse->rtable;
CmdType command_type = parse->commandType;
ListCell *lc;
*************** preprocess_targetlist(PlannerInfo *root,
*** 73,80 ****
* 10/94
*/
if (command_type == CMD_INSERT || command_type == CMD_UPDATE)
tlist = expand_targetlist(tlist, command_type,
! result_relation, range_table);
/*
* Add necessary junk columns for rowmarked rels. These values are needed
--- 115,126 ----
* 10/94
*/
if (command_type == CMD_INSERT || command_type == CMD_UPDATE)
+ {
tlist = expand_targetlist(tlist, command_type,
! result_relation,
! source_relation,
! range_table);
! }
/*
* Add necessary junk columns for rowmarked rels. These values are needed
*************** preprocess_targetlist(PlannerInfo *root,
*** 96,102 ****
{
/* It's a regular table, so fetch its TID */
var = makeVar(rc->rti,
! SelfItemPointerAttributeNumber,
TIDOID,
-1,
InvalidOid,
--- 142,149 ----
{
/* It's a regular table, so fetch its TID */
var = makeVar(rc->rti,
! lookup_varattno(SelfItemPointerAttributeNumber,
! rc->rti, range_table),
TIDOID,
-1,
InvalidOid,
*************** preprocess_targetlist(PlannerInfo *root,
*** 112,118 ****
if (rc->isParent)
{
var = makeVar(rc->rti,
! TableOidAttributeNumber,
OIDOID,
-1,
InvalidOid,
--- 159,166 ----
if (rc->isParent)
{
var = makeVar(rc->rti,
! lookup_varattno(TableOidAttributeNumber,
! rc->rti, range_table),
OIDOID,
-1,
InvalidOid,
*************** preprocess_targetlist(PlannerInfo *root,
*** 195,201 ****
*/
static List *
expand_targetlist(List *tlist, int command_type,
! Index result_relation, List *range_table)
{
List *new_tlist = NIL;
ListCell *tlist_item;
--- 243,250 ----
*/
static List *
expand_targetlist(List *tlist, int command_type,
! Index result_relation, Index source_relation,
! List *range_table)
{
List *new_tlist = NIL;
ListCell *tlist_item;
*************** expand_targetlist(List *tlist, int comma
*** 218,223 ****
--- 267,275 ----
numattrs = RelationGetNumberOfAttributes(rel);
+ if (source_relation == 0)
+ source_relation = result_relation;
+
for (attrno = 1; attrno <= numattrs; attrno++)
{
Form_pg_attribute att_tup = rel->rd_att->attrs[attrno - 1];
*************** expand_targetlist(List *tlist, int comma
*** 298,305 ****
case CMD_UPDATE:
if (!att_tup->attisdropped)
{
! new_expr = (Node *) makeVar(result_relation,
! attrno,
atttype,
atttypmod,
attcollation,
--- 350,359 ----
case CMD_UPDATE:
if (!att_tup->attisdropped)
{
! new_expr = (Node *) makeVar(source_relation,
! lookup_varattno(attrno,
! source_relation,
! range_table),
atttype,
atttypmod,
attcollation,
diff --git a/src/backend/optimizer/prep/prepunion.c b/src/backend/optimizer/prep/prepunion.c
new file mode 100644
index e249628..017bc2c
*** a/src/backend/optimizer/prep/prepunion.c
--- b/src/backend/optimizer/prep/prepunion.c
*************** typedef struct
*** 55,60 ****
--- 55,61 ----
{
PlannerInfo *root;
AppendRelInfo *appinfo;
+ bool in_returning;
} adjust_appendrel_attrs_context;
static Plan *recurse_set_operations(Node *setOp, PlannerInfo *root,
*************** adjust_appendrel_attrs(PlannerInfo *root
*** 1594,1599 ****
--- 1595,1601 ----
context.root = root;
context.appinfo = appinfo;
+ context.in_returning = false;
/*
* Must be prepared to start with a Query or a bare expression tree.
*************** adjust_appendrel_attrs(PlannerInfo *root
*** 1605,1614 ****
newnode = query_tree_mutator((Query *) node,
adjust_appendrel_attrs_mutator,
(void *) &context,
! QTW_IGNORE_RC_SUBQUERIES);
if (newnode->resultRelation == appinfo->parent_relid)
{
! newnode->resultRelation = appinfo->child_relid;
/* Fix tlist resnos too, if it's inherited UPDATE */
if (newnode->commandType == CMD_UPDATE)
newnode->targetList =
--- 1607,1635 ----
newnode = query_tree_mutator((Query *) node,
adjust_appendrel_attrs_mutator,
(void *) &context,
! QTW_IGNORE_RC_SUBQUERIES |
! QTW_IGNORE_RETURNING);
! /*
! * Returning clause on the relation being replaced with row-
! * security subquery shall be handled in a special way, because
! * of no system columns on subquery.
! * Var references to system column or whole-row reference need
! * to be adjusted to reference pseudo columns on behalf of
! * the underlying these columns, however, RETURNGIN clause is
! * an exception because its Var nodes are evaluated towards
! * the "raw" target relation, not a fetched tuple.
! */
! context.in_returning = true;
! newnode->returningList = (List *)
! expression_tree_mutator((Node *) newnode->returningList,
! adjust_appendrel_attrs_mutator,
! (void *) &context);
if (newnode->resultRelation == appinfo->parent_relid)
{
! newnode->resultRelation = (appinfo->child_result > 0 ?
! appinfo->child_result :
! appinfo->child_relid);
! newnode->sourceRelation = appinfo->child_relid;
/* Fix tlist resnos too, if it's inherited UPDATE */
if (newnode->commandType == CMD_UPDATE)
newnode->targetList =
*************** adjust_appendrel_attrs(PlannerInfo *root
*** 1624,1629 ****
--- 1645,1693 ----
}
static Node *
+ fixup_var_on_rowsec_subquery(RangeTblEntry *rte, Var *var)
+ {
+ ListCell *cell;
+
+ Assert(rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_SECURITY);
+ /*
+ * In case when row-level security policy is applied on the referenced
+ * table, its RangeTblEntry (RTE_RELATION) is replaced with sub-query
+ * to filter out unprivileged rows of underlying relation.
+ * Even though reference to this sub-query should perform as if ones
+ * to real relations, system column has to be cared in special way
+ * due to the nature of sub-query.
+ * Target-entries that reference system columns should be added on
+ * rowlevelsec.c, so all we need to do here is looking up underlying
+ * target-list that can reference underlying system column, and fix-
+ * up varattno of the referencing Var node with resno of TargetEntry.
+ */
+ foreach (cell, rte->subquery->targetList)
+ {
+ TargetEntry *subtle = lfirst(cell);
+
+ if (IsA(subtle->expr, Var))
+ {
+ Var *subvar = (Var *) subtle->expr;
+ Var *newnode;
+
+ if (subvar->varattno == var->varattno)
+ {
+ newnode = copyObject(var);
+ newnode->varattno = subtle->resno;
+ return (Node *)newnode;
+ }
+ }
+ else
+ Assert(IsA(subtle->expr, Const));
+ }
+ elog(ERROR, "could not find pseudo column of %d, relid %u",
+ var->varattno, var->varno);
+ return NULL;
+ }
+
+ static Node *
adjust_appendrel_attrs_mutator(Node *node,
adjust_appendrel_attrs_context *context)
{
*************** adjust_appendrel_attrs_mutator(Node *nod
*** 1638,1645 ****
if (var->varlevelsup == 0 &&
var->varno == appinfo->parent_relid)
{
! var->varno = appinfo->child_relid;
var->varnoold = appinfo->child_relid;
if (var->varattno > 0)
{
Node *newnode;
--- 1702,1713 ----
if (var->varlevelsup == 0 &&
var->varno == appinfo->parent_relid)
{
! var->varno = (context->in_returning &&
! appinfo->child_result > 0 ?
! appinfo->child_result :
! appinfo->child_relid);
var->varnoold = appinfo->child_relid;
+
if (var->varattno > 0)
{
Node *newnode;
*************** adjust_appendrel_attrs_mutator(Node *nod
*** 1664,1669 ****
--- 1732,1745 ----
*/
if (OidIsValid(appinfo->child_reltype))
{
+ Query *parse = context->root->parse;
+ RangeTblEntry *rte = rt_fetch(appinfo->child_relid,
+ parse->rtable);
+ if (!context->in_returning &&
+ rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_SECURITY)
+ var = (Var *)fixup_var_on_rowsec_subquery(rte, var);
+
Assert(var->vartype == appinfo->parent_reltype);
if (appinfo->parent_reltype != appinfo->child_reltype)
{
*************** adjust_appendrel_attrs_mutator(Node *nod
*** 1708,1714 ****
return (Node *) rowexpr;
}
}
! /* system attributes don't need any other translation */
}
return (Node *) var;
}
--- 1784,1801 ----
return (Node *) rowexpr;
}
}
! else
! {
! Query *parse = context->root->parse;
! RangeTblEntry *rte;
!
! rte = rt_fetch(appinfo->child_relid, parse->rtable);
!
! if (!context->in_returning &&
! rte->rtekind == RTE_SUBQUERY &&
! rte->subquery->querySource == QSRC_ROW_SECURITY)
! return fixup_var_on_rowsec_subquery(rte, var);
! }
}
return (Node *) var;
}
diff --git a/src/backend/optimizer/util/Makefile b/src/backend/optimizer/util/Makefile
new file mode 100644
index 3b2d16b..3f5cb19
*** a/src/backend/optimizer/util/Makefile
--- b/src/backend/optimizer/util/Makefile
*************** top_builddir = ../../../..
*** 13,18 ****
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
include $(top_srcdir)/src/backend/common.mk
--- 13,18 ----
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 rowsecurity.o
include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/optimizer/util/rowsecurity.c b/src/backend/optimizer/util/rowsecurity.c
new file mode 100644
index ...c2e5a49
*** a/src/backend/optimizer/util/rowsecurity.c
--- b/src/backend/optimizer/util/rowsecurity.c
***************
*** 0 ****
--- 1,744 ----
+ /*
+ * optimizer/util/rowsecurity.c
+ * Routines to support row-security feature
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ */
+ #include "postgres.h"
+
+ #include "access/heapam.h"
+ #include "access/htup_details.h"
+ #include "access/sysattr.h"
+ #include "catalog/pg_class.h"
+ #include "catalog/pg_inherits_fn.h"
+ #include "catalog/pg_rowsecurity.h"
+ #include "catalog/pg_type.h"
+ #include "miscadmin.h"
+ #include "nodes/makefuncs.h"
+ #include "nodes/nodeFuncs.h"
+ #include "nodes/plannodes.h"
+ #include "optimizer/clauses.h"
+ #include "optimizer/prep.h"
+ #include "optimizer/rowsecurity.h"
+ #include "parser/parsetree.h"
+ #include "rewrite/rewriteHandler.h"
+ #include "utils/lsyscache.h"
+ #include "utils/rel.h"
+ #include "utils/syscache.h"
+ #include "tcop/utility.h"
+
+ /* flags to pull row-security policy */
+ #define RSEC_FLAG_HAS_SUBLINKS 0x0001
+
+ /* hook to allow extensions to apply their own security policy */
+ row_security_policy_hook_type row_security_policy_hook = NULL;
+
+ /*
+ * make_pseudo_column
+ *
+ * It makes a target-entry node that references underlying column.
+ * Its tle->expr is usualy Var node, but may be Const for dummy NULL
+ * if the supplied attribute was already dropped.
+ */
+ static TargetEntry *
+ make_pseudo_column(RangeTblEntry *subrte, AttrNumber attnum)
+ {
+ Expr *expr;
+ char *resname;
+
+ Assert(subrte->rtekind == RTE_RELATION && OidIsValid(subrte->relid));
+ if (attnum == InvalidAttrNumber)
+ {
+ expr = (Expr *) makeWholeRowVar(subrte, (Index) 1, 0, false);
+ resname = get_rel_name(subrte->relid);
+ }
+ else
+ {
+ HeapTuple tuple;
+ Form_pg_attribute attform;
+
+ tuple = SearchSysCache2(ATTNUM,
+ ObjectIdGetDatum(subrte->relid),
+ Int16GetDatum(attnum));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for attribute %d of relation %u",
+ attnum, subrte->relid);
+ attform = (Form_pg_attribute) GETSTRUCT(tuple);
+
+ if (attform->attisdropped)
+ {
+ char namebuf[NAMEDATALEN];
+
+ /* Insert NULL just for a placeholder of dropped column */
+ expr = (Expr *) makeConst(INT4OID,
+ -1,
+ InvalidOid,
+ sizeof(int32),
+ (Datum) 0,
+ true, /* isnull */
+ true); /* byval */
+ sprintf(namebuf, "dummy-%d", (int)attform->attnum);
+ resname = pstrdup(namebuf);
+ }
+ else
+ {
+ expr = (Expr *) makeVar((Index) 1,
+ attform->attnum,
+ attform->atttypid,
+ attform->atttypmod,
+ attform->attcollation,
+ 0);
+ resname = pstrdup(NameStr(attform->attname));
+ }
+ ReleaseSysCache(tuple);
+ }
+ return makeTargetEntry(expr, -1, resname, false);
+ }
+
+ /*
+ * lookup_pseudo_column
+ *
+ * It looks-up resource number of the target-entry relevant to the given
+ * Var-node that references the row-security subquery. If required column
+ * is not in the subquery's target-list, this function also adds new one
+ * and returns its resource number.
+ */
+ static AttrNumber
+ lookup_pseudo_column(PlannerInfo *root,
+ RangeTblEntry *rte, AttrNumber varattno)
+ {
+ Query *subqry;
+ RangeTblEntry *subrte;
+ TargetEntry *subtle;
+ ListCell *cell;
+
+ Assert(rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_SECURITY);
+
+ subqry = rte->subquery;
+ foreach (cell, subqry->targetList)
+ {
+ subtle = lfirst(cell);
+
+ /*
+ * If referenced artifical column is already constructed on the
+ * target-list of row-security subquery, nothing to do any more.
+ */
+ if (IsA(subtle->expr, Var))
+ {
+ Var *subvar = (Var *)subtle->expr;
+
+ Assert(subvar->varno == 1);
+ if (subvar->varattno == varattno)
+ return subtle->resno;
+ }
+ }
+
+ /*
+ * OK, we don't have an artifical column relevant to the required ones,
+ * so let's create a new artifical column on demand.
+ */
+ subrte = rt_fetch((Index) 1, subqry->rtable);
+ subtle = make_pseudo_column(subrte, varattno);
+ subtle->resno = list_length(subqry->targetList) + 1;
+
+ subqry->targetList = lappend(subqry->targetList, subtle);
+ rte->eref->colnames = lappend(rte->eref->colnames,
+ makeString(pstrdup(subtle->resname)));
+ return subtle->resno;
+ }
+
+ /*
+ * fixup_varnode_walker
+ *
+ * It recursively fixes up references to the relation to be replaced by
+ * row-security sub-query, and adds pseudo columns relevant to the
+ * underlying system columns or whole row-reference on demand.
+ */
+ typedef struct {
+ PlannerInfo *root;
+ int varlevelsup;
+ Index *vartrans;
+ } fixup_varnode_context;
+
+ static bool
+ fixup_varnode_walker(Node *node, fixup_varnode_context *context)
+ {
+ if (node == NULL)
+ return false;
+
+ if (IsA(node, Var))
+ {
+ Var *var = (Var *) node;
+ List *rtable = context->root->parse->rtable;
+ RangeTblEntry *rte;
+ ListCell *cell;
+
+ /*
+ * Ignore it, if Var node does not reference the Query currently
+ * we focus on.
+ */
+ if (var->varlevelsup != context->varlevelsup)
+ return false;
+
+ if (context->vartrans[var->varno] > 0)
+ {
+ Index rtindex_trans = context->vartrans[var->varno];
+
+ rte = rt_fetch(rtindex_trans, rtable);
+ Assert(rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_SECURITY);
+
+ var->varno = var->varnoold = rtindex_trans;
+ var->varattno = lookup_pseudo_column(context->root, rte,
+ var->varattno);
+ }
+ else
+ {
+ rte = rt_fetch(var->varno, rtable);
+ if (rte->rtekind == RTE_RELATION && rte->inh)
+ {
+ foreach (cell, context->root->append_rel_list)
+ {
+ AppendRelInfo *appinfo = lfirst(cell);
+ RangeTblEntry *child_rte;
+
+ if (appinfo->parent_relid != var->varno)
+ continue;
+
+ child_rte = rt_fetch(appinfo->child_relid, rtable);
+ if (child_rte->rtekind == RTE_SUBQUERY &&
+ child_rte->subquery->querySource == QSRC_ROW_SECURITY)
+ (void) lookup_pseudo_column(context->root,
+ child_rte,
+ var->varattno);
+ }
+ }
+ }
+ }
+ else if (IsA(node, RangeTblRef))
+ {
+ RangeTblRef *rtr = (RangeTblRef *) node;
+
+ if (context->varlevelsup == 0 &&
+ context->vartrans[rtr->rtindex] != 0)
+ rtr->rtindex = context->vartrans[rtr->rtindex];
+ }
+ else if (IsA(node, Query))
+ {
+ bool result;
+
+ context->varlevelsup++;
+ result = query_tree_walker((Query *) node,
+ fixup_varnode_walker,
+ (void *) context, 0);
+ context->varlevelsup--;
+
+ return result;
+ }
+ return expression_tree_walker(node,
+ fixup_varnode_walker,
+ (void *) context);
+ }
+
+ /*
+ * check_infinite_recursion
+ *
+ * It is a wrong row-security configuration, if we try to expand
+ * the relation inside of row-security subquery originated from
+ * same relation!
+ */
+ static void
+ check_infinite_recursion(PlannerInfo *root, Oid relid)
+ {
+ PlannerInfo *parent = root->parent_root;
+
+ if (parent && parent->parse->querySource == QSRC_ROW_SECURITY)
+ {
+ RangeTblEntry *rte = rt_fetch(1, parent->parse->rtable);
+
+ Assert(rte->rtekind == RTE_RELATION && OidIsValid(rte->relid));
+
+ if (relid == rte->relid)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("infinite recursion detected for relation \"%s\"",
+ get_rel_name(relid))));
+ check_infinite_recursion(parent, relid);
+ }
+ }
+
+ /*
+ * expand_rtentry_with_policy
+ *
+ * It extends a range-table entry of row-security sub-query with supplied
+ * security policy, and append it on the parse->rtable.
+ * This sub-query contains pseudo columns that reference underlying
+ * regular columns (at least, references to system column or whole of
+ * table reference shall be added on demand), and simple scan on the
+ * target relation.
+ * Any Var nodes that referenced the relation pointed by rtindex shall
+ * be adjusted to reference this sub-query instead. walker
+ */
+ static Index
+ expand_rtentry_with_policy(PlannerInfo *root, Index rtindex,
+ Expr *qual, int flags)
+ {
+ Query *parse = root->parse;
+ RangeTblEntry *rte = rt_fetch(rtindex, parse->rtable);
+ Query *subqry;
+ RangeTblEntry *subrte;
+ RangeTblRef *subrtr;
+ TargetEntry *subtle;
+ RangeTblEntry *newrte;
+ HeapTuple tuple;
+ AttrNumber nattrs;
+ AttrNumber attnum;
+ List *targetList = NIL;
+ List *colNameList = NIL;
+ PlanRowMark *rowmark;
+
+ Assert(rte->rtekind == RTE_RELATION && !rte->inh);
+
+ /* check recursion to prevent infinite loop */
+ check_infinite_recursion(root, rte->relid);
+
+ /* Expand views inside SubLink node */
+ if (flags & RSEC_FLAG_HAS_SUBLINKS)
+ QueryRewriteExpr((Node *)qual, list_make1_oid(rte->relid));
+
+ /*
+ * Construction of sub-query
+ */
+ subqry = (Query *) makeNode(Query);
+ subqry->commandType = CMD_SELECT;
+ subqry->querySource = QSRC_ROW_SECURITY;
+
+ subrte = copyObject(rte);
+ subqry->rtable = list_make1(subrte);
+
+ subrtr = makeNode(RangeTblRef);
+ subrtr->rtindex = 1;
+ subqry->jointree = makeFromExpr(list_make1(subrtr), (Node *) qual);
+ if (flags & RSEC_FLAG_HAS_SUBLINKS)
+ subqry->hasSubLinks = true;
+
+ /*
+ * Construction of TargetEntries that reference underlying columns.
+ */
+ tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(rte->relid));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for relation %u", rte->relid);
+ nattrs = ((Form_pg_class) GETSTRUCT(tuple))->relnatts;
+ ReleaseSysCache(tuple);
+
+ for (attnum = 1; attnum <= nattrs; attnum++)
+ {
+ subtle = make_pseudo_column(subrte, attnum);
+ subtle->resno = list_length(targetList) + 1;
+ Assert(subtle->resno == attnum);
+
+ targetList = lappend(targetList, subtle);
+ colNameList = lappend(colNameList,
+ makeString(pstrdup(subtle->resname)));
+ }
+ subqry->targetList = targetList;
+
+ /* Expand RengeTblEntry with this sub-query */
+ newrte = makeNode(RangeTblEntry);
+ newrte->rtekind = RTE_SUBQUERY;
+ newrte->subquery = subqry;
+ newrte->security_barrier = true;
+ newrte->rowsec_relid = rte->relid;
+ newrte->eref = makeAlias(get_rel_name(rte->relid), colNameList);
+
+ parse->rtable = lappend(parse->rtable, newrte);
+
+ /*
+ * Fix up PlanRowMark if needed, then add references to 'tableoid' and
+ * 'ctid' that shall be added to handle row-level locking.
+ * Also see preprocess_targetlist() that adds some junk attributes.
+ */
+ rowmark = get_plan_rowmark(root->rowMarks, rtindex);
+ if (rowmark)
+ {
+ if (rowmark->rti == rowmark->prti)
+ rowmark->rti = rowmark->prti = list_length(parse->rtable);
+ else
+ rowmark->rti = list_length(parse->rtable);
+
+ lookup_pseudo_column(root, newrte, SelfItemPointerAttributeNumber);
+ lookup_pseudo_column(root, newrte, TableOidAttributeNumber);
+ }
+ return list_length(parse->rtable);
+ }
+
+ /*
+ * pull_row_security_policy
+ *
+ * It pulls the configured row-security policy of both built-in and
+ * extensions. If any, it returns expression tree.
+ */
+ static Expr *
+ pull_row_security_policy(CmdType cmd, Relation relation, int *p_flags)
+ {
+ Expr *quals = NULL;
+ int flags = 0;
+
+ /*
+ * Pull the row-security policy configured with built-in features,
+ * if unprivileged users. Please note that superuser can bypass it.
+ */
+ if (relation->rsdesc && !superuser())
+ {
+ RowSecurityDesc *rsdesc = relation->rsdesc;
+
+ quals = copyObject(rsdesc->rsall.qual);
+ if (rsdesc->rsall.hassublinks)
+ flags |= RSEC_FLAG_HAS_SUBLINKS;
+ }
+
+ /*
+ * Also, ask extensions whether they want to apply their own
+ * row-security policy. If both built-in and extension has
+ * their own policy, it shall be merged.
+ */
+ if (row_security_policy_hook)
+ {
+ List *temp;
+
+ temp = (*row_security_policy_hook)(cmd, relation);
+ if (temp != NIL)
+ {
+ if ((flags & RSEC_FLAG_HAS_SUBLINKS) == 0 &&
+ contain_subplans((Node *) temp))
+ flags |= RSEC_FLAG_HAS_SUBLINKS;
+
+ if (quals != NULL)
+ temp = lappend(temp, quals);
+
+ if (list_length(temp) == 1)
+ quals = (Expr *)list_head(temp);
+ else if (list_length(temp) > 1)
+ quals = makeBoolExpr(AND_EXPR, temp, -1);
+ }
+ }
+ *p_flags = flags;
+ return quals;
+ }
+
+ /*
+ * copy_row_security_policy
+ *
+ * It construct a row-security subquery instead of raw COPY TO statement,
+ * if target relation has a row-level security policy
+ */
+ bool
+ copy_row_security_policy(CopyStmt *stmt, Relation rel, List *attnums)
+ {
+ Expr *quals;
+ int flags;
+ Query *parse;
+ RangeTblEntry *rte;
+ RangeTblRef *rtr;
+ TargetEntry *tle;
+ Var *var;
+ ListCell *cell;
+
+ if (stmt->is_from)
+ return false;
+
+ quals = pull_row_security_policy(CMD_SELECT, rel, &flags);
+ if (!quals)
+ return false;
+
+ parse = (Query *) makeNode(Query);
+ parse->commandType = CMD_SELECT;
+ parse->querySource = QSRC_ROW_SECURITY;
+
+ rte = makeNode(RangeTblEntry);
+ rte->rtekind = RTE_RELATION;
+ rte->relid = RelationGetRelid(rel);
+ rte->relkind = RelationGetForm(rel)->relkind;
+
+ foreach (cell, attnums)
+ {
+ HeapTuple tuple;
+ Form_pg_attribute attform;
+ AttrNumber attno = lfirst_int(cell);
+
+ tuple = SearchSysCache2(ATTNUM,
+ ObjectIdGetDatum(RelationGetRelid(rel)),
+ Int16GetDatum(attno));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for attribute %d of relation %s",
+ attno, RelationGetRelationName(rel));
+ attform = (Form_pg_attribute) GETSTRUCT(tuple);
+
+ var = makeVar((Index) 1,
+ attform->attnum,
+ attform->atttypid,
+ attform->atttypmod,
+ attform->attcollation,
+ 0);
+ tle = makeTargetEntry((Expr *) var,
+ list_length(parse->targetList) + 1,
+ pstrdup(NameStr(attform->attname)),
+ false);
+ parse->targetList = lappend(parse->targetList, tle);
+
+ ReleaseSysCache(tuple);
+
+ rte->selectedCols = bms_add_member(rte->selectedCols,
+ attno - FirstLowInvalidHeapAttributeNumber);
+ }
+ rte->inFromCl = true;
+ rte->requiredPerms = ACL_SELECT;
+
+ rtr = makeNode(RangeTblRef);
+ rtr->rtindex = 1;
+
+ parse->jointree = makeFromExpr(list_make1(rtr), (Node *) quals);
+ parse->rtable = list_make1(rte);
+ if (flags & RSEC_FLAG_HAS_SUBLINKS)
+ parse->hasSubLinks = true;
+
+ stmt->query = (Node *) parse;
+
+ return true;
+ }
+
+ /*
+ * apply_row_security_relation
+ *
+ * It applies row-security policy on a particular relation being specified.
+ * If this relation is top of the inheritance tree, it also checks inherited
+ * children.
+ */
+ static bool
+ apply_row_security_relation(PlannerInfo *root, Index *vartrans,
+ CmdType cmd, Index rtindex)
+ {
+ Query *parse = root->parse;
+ RangeTblEntry *rte = rt_fetch(rtindex, parse->rtable);
+ Relation rel;
+ Expr *qual;
+ int flags;
+ bool result = false;
+
+ if (!rte->inh)
+ {
+ rel = heap_open(rte->relid, NoLock);
+ qual = pull_row_security_policy(cmd, rel, &flags);
+ if (qual)
+ {
+ vartrans[rtindex]
+ = expand_rtentry_with_policy(root, rtindex, qual, flags);
+ if (parse->resultRelation == rtindex)
+ parse->sourceRelation = vartrans[rtindex];
+ result = true;
+ }
+ heap_close(rel, NoLock);
+ }
+ else
+ {
+ /*
+ * In case when relation has inherited children, we try to apply
+ * row-level security policy of them if configured.
+ * In addition to regular replacement with a sub-query, we need
+ * to adjust rtindex of AppendRelInfo and varno of translated_vars.
+ * It makes sub-queries perform like regular relations being
+ * inherited from a particular parent relation. So, a table scan
+ * may have underlying a relation scan and two sub-query scans for
+ * instance. If it is result relation of UPDATE or DELETE command,
+ * rtindex to the original relation (regular relation) has to be
+ * kept because sub-query cannot perform as an updatable relation.
+ * So, we save it on child_result of AppendRelInfo; that shall be
+ * used to track relations to be modified at inheritance_planner().
+ */
+ ListCell *lc1, *lc2;
+
+ foreach (lc1, root->append_rel_list)
+ {
+ AppendRelInfo *apinfo = lfirst(lc1);
+
+ if (apinfo->parent_relid != rtindex)
+ continue;
+
+ if (apply_row_security_relation(root, vartrans, cmd,
+ apinfo->child_relid))
+ {
+ /*
+ * Save the rtindex of actual relation to be modified,
+ * if parent relation is result relation of this query.
+ */
+ if (parse->resultRelation == rtindex)
+ apinfo->child_result = apinfo->child_relid;
+
+ apinfo->child_relid = vartrans[apinfo->child_relid];
+ /* Adjust varno to reference pseudo columns */
+ foreach (lc2, apinfo->translated_vars)
+ {
+ Var *var = lfirst(lc2);
+
+ if (var)
+ var->varno = apinfo->child_relid;
+ }
+ result = true;
+ }
+ }
+ }
+ return result;
+ }
+
+ /*
+ * apply_row_security_recursive
+ *
+ * It walks on the given join-tree to replace relations with row-level
+ * security policy by a simple sub-query.
+ */
+ static bool
+ apply_row_security_recursive(PlannerInfo *root, Index *vartrans, Node *jtnode)
+ {
+ bool result = false;
+
+ if (jtnode == NULL)
+ return false;
+ if (IsA(jtnode, RangeTblRef))
+ {
+ Index rtindex = ((RangeTblRef *) jtnode)->rtindex;
+ Query *parse = root->parse;
+ RangeTblEntry *rte = rt_fetch(rtindex, parse->rtable);
+ CmdType cmd;
+
+ /* Only relation can have row-security policy */
+ if (rte->rtekind != RTE_RELATION)
+ return false;
+
+ /*
+ * Prevents infinite recursion. Please note that rtindex == 1
+ * of the row-security subquery is a relation being already
+ * processed on the upper level.
+ */
+ if (parse->querySource == QSRC_ROW_SECURITY && rtindex == 1)
+ return false;
+
+ /* Is it a result relation of UPDATE or DELETE command? */
+ if (parse->resultRelation == rtindex)
+ cmd = parse->commandType;
+ else
+ cmd = CMD_SELECT;
+
+ /* Try to apply row-security policy, if configured */
+ result = apply_row_security_relation(root, vartrans, cmd, rtindex);
+ }
+ else if (IsA(jtnode, FromExpr))
+ {
+ FromExpr *f = (FromExpr *) jtnode;
+ ListCell *l;
+
+ foreach (l, f->fromlist)
+ {
+ if (apply_row_security_recursive(root, vartrans, lfirst(l)))
+ result = true;
+ }
+ }
+ else if (IsA(jtnode, JoinExpr))
+ {
+ JoinExpr *j = (JoinExpr *) jtnode;
+
+ if (apply_row_security_recursive(root, vartrans, j->larg))
+ result = true;
+ if (apply_row_security_recursive(root, vartrans, j->rarg))
+ result = true;
+ }
+ else
+ elog(ERROR, "unexpected node type: %d", (int) nodeTag(jtnode));
+
+ return result;
+ }
+
+ /*
+ * apply_row_security_policy
+ *
+ * Entrypoint to apply configured row-security policy of the relation.
+ *
+ * In case when the supplied query references relations with row-security
+ * policy, its RangeTblEntry shall be replaced by a row-security subquery
+ * that has simple scan on the referenced table with policy qualifiers.
+ * Of course, security-barrier shall be set on the subquery to prevent
+ * unexpected push-down of functions without leakproof flag.
+ *
+ * For example, when table t1 has a security policy "(x % 2 = 0)", the
+ * following query:
+ * SELECT * FROM t1 WHERE f_leak(y)
+ * performs as if
+ * SELECT * FROM (
+ * SELECT x, y FROM t1 WHERE (x % 2 = 0)
+ * ) AS t1 WHERE f_leak(y)
+ * would be given. Because the sub-query has security barrier flag,
+ * configured security policy qualifier is always executed prior to
+ * user given functions.
+ */
+ void
+ apply_row_security_policy(PlannerInfo *root)
+ {
+ Query *parse = root->parse;
+ Oid curr_userid;
+ int curr_seccxt;
+ Index *vartrans;
+
+ /*
+ * Mode checks. In case when SECURITY_ROW_LEVEL_DISABLED is set,
+ * no row-level security policy should be applied regardless
+ * whether it is built-in or extension.
+ */
+ GetUserIdAndSecContext(&curr_userid, &curr_seccxt);
+ if (curr_seccxt & SECURITY_ROW_LEVEL_DISABLED)
+ return;
+
+ vartrans = palloc0(sizeof(Index) * (list_length(parse->rtable) + 1));
+ if (apply_row_security_recursive(root, vartrans, (Node *)parse->jointree))
+ {
+ PlannerGlobal *glob = root->glob;
+ PlanInvalItem *pi_item;
+ fixup_varnode_context context;
+
+ /*
+ * Constructed Plan with row-level security policy depends on
+ * properties of current user (database superuser can bypass
+ * configured row-security policy!), thus, it has to be
+ * invalidated when its assumption was changed.
+ */
+ if (!OidIsValid(glob->planUserId))
+ {
+ /* Plan invalidation on session user-id */
+ glob->planUserId = GetUserId();
+
+ /* Plan invalidation on catalog updates of pg_authid */
+ pi_item = makeNode(PlanInvalItem);
+ pi_item->cacheId = AUTHOID;
+ pi_item->hashValue =
+ GetSysCacheHashValue1(AUTHOID,
+ ObjectIdGetDatum(glob->planUserId));
+ glob->invalItems = lappend(glob->invalItems, pi_item);
+ }
+ else
+ Assert(glob->planUserId == GetUserId());
+
+ /*
+ * Var-nodes that referenced RangeTblEntry to be replaced by
+ * row-security sub-query have to be adjusted for appropriate
+ * reference to the underlying pseudo column of the relation.
+ */
+ context.root = root;
+ context.varlevelsup = 0;
+ context.vartrans = vartrans;
+ query_tree_walker(parse,
+ fixup_varnode_walker,
+ (void *) &context,
+ QTW_IGNORE_RETURNING);
+ }
+ pfree(vartrans);
+ }
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
new file mode 100644
index 1922097..2c22de8
*** a/src/backend/parser/gram.y
--- b/src/backend/parser/gram.y
*************** static Node *makeRecursiveViewSelect(cha
*** 257,262 ****
--- 257,263 ----
%type <node> alter_table_cmd alter_type_cmd opt_collate_clause
replica_identity
%type <list> alter_table_cmds alter_type_cmds
+ %type <str> row_security_cmd
%type <dbehavior> opt_drop_behavior
*************** alter_table_cmd:
*** 2183,2188 ****
--- 2184,2206 ----
n->def = (Node *)$2;
$$ = (Node *)n;
}
+ /* ALTER TABLE <name> SET ROW SECURITY FOR <cmd> TO (<expr>) */
+ | SET ROW SECURITY FOR row_security_cmd TO '(' a_expr ')'
+ {
+ AlterTableCmd *n = makeNode(AlterTableCmd);
+ n->subtype = AT_SetRowSecurity;
+ n->name = $5;
+ n->def = (Node *) $8;
+ $$ = (Node *)n;
+ }
+ /* ALTER TABLE <name> RESET ROW SECURITY FOR <cmd> */
+ | RESET ROW SECURITY FOR row_security_cmd
+ {
+ AlterTableCmd *n = makeNode(AlterTableCmd);
+ n->subtype = AT_ResetRowSecurity;
+ n->name = $5;
+ n->def = NULL;
+ }
/* ALTER TABLE <name> REPLICA IDENTITY */
| REPLICA IDENTITY_P replica_identity
{
*************** reloption_elem:
*** 2293,2298 ****
--- 2311,2322 ----
}
;
+ row_security_cmd: ALL { $$ = "all"; }
+ | SELECT { $$ = "select"; }
+ | INSERT { $$ = "insert"; }
+ | UPDATE { $$ = "update"; }
+ | DELETE_P { $$ = "delete"; }
+ ;
/*****************************************************************************
*
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
new file mode 100644
index 98cb58a..ac50a2e
*** a/src/backend/parser/parse_agg.c
--- b/src/backend/parser/parse_agg.c
*************** transformAggregateCall(ParseState *pstat
*** 272,277 ****
--- 272,280 ----
case EXPR_KIND_TRIGGER_WHEN:
err = _("aggregate functions are not allowed in trigger WHEN conditions");
break;
+ case EXPR_KIND_ROW_SECURITY:
+ err = _("aggregate functions are not allowed in row-security policy");
+ break;
/*
* There is intentionally no default: case here, so that the
*************** transformWindowFuncCall(ParseState *psta
*** 547,552 ****
--- 550,558 ----
case EXPR_KIND_TRIGGER_WHEN:
err = _("window functions are not allowed in trigger WHEN conditions");
break;
+ case EXPR_KIND_ROW_SECURITY:
+ err = _("window functions are not allowed in row-security policy");
+ break;
/*
* There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
new file mode 100644
index 68b711d..fb09c8c
*** a/src/backend/parser/parse_expr.c
--- b/src/backend/parser/parse_expr.c
*************** transformSubLink(ParseState *pstate, Sub
*** 1459,1464 ****
--- 1459,1465 ----
case EXPR_KIND_OFFSET:
case EXPR_KIND_RETURNING:
case EXPR_KIND_VALUES:
+ case EXPR_KIND_ROW_SECURITY:
/* okay */
break;
case EXPR_KIND_CHECK_CONSTRAINT:
*************** ParseExprKindName(ParseExprKind exprKind
*** 2640,2645 ****
--- 2641,2648 ----
return "EXECUTE";
case EXPR_KIND_TRIGGER_WHEN:
return "WHEN";
+ case EXPR_KIND_ROW_SECURITY:
+ return "ROW SECURITY";
/*
* There is intentionally no default: case here, so that the
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
new file mode 100644
index 50cb753..f549293
*** a/src/backend/rewrite/rewriteHandler.c
--- b/src/backend/rewrite/rewriteHandler.c
*************** QueryRewrite(Query *parsetree)
*** 3282,3284 ****
--- 3282,3300 ----
return results;
}
+
+ /*
+ * QueryRewriteExpr
+ *
+ * This routine provides an entry point of query rewriter towards
+ * a certain expression tree with SubLink node; being added after
+ * the top level query rewrite.
+ * It primarily intends to expand views appeared in the qualifiers
+ * appended with row-level security which needs to modify query
+ * tree at head of the planner stage.
+ */
+ void
+ QueryRewriteExpr(Node *node, List *activeRIRs)
+ {
+ fireRIRonSubLink(node, activeRIRs);
+ }
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
new file mode 100644
index 917130f..b24af71
*** a/src/backend/utils/adt/ri_triggers.c
--- b/src/backend/utils/adt/ri_triggers.c
*************** ri_PerformCheck(const RI_ConstraintInfo
*** 3008,3013 ****
--- 3008,3014 ----
int spi_result;
Oid save_userid;
int save_sec_context;
+ int temp_sec_context;
Datum vals[RI_MAX_NUMKEYS * 2];
char nulls[RI_MAX_NUMKEYS * 2];
*************** ri_PerformCheck(const RI_ConstraintInfo
*** 3087,3094 ****
/* Switch to proper UID to perform check as */
GetUserIdAndSecContext(&save_userid, &save_sec_context);
SetUserIdAndSecContext(RelationGetForm(query_rel)->relowner,
! save_sec_context | SECURITY_LOCAL_USERID_CHANGE);
/* Finally we can run the query. */
spi_result = SPI_execute_snapshot(qplan,
--- 3088,3105 ----
/* Switch to proper UID to perform check as */
GetUserIdAndSecContext(&save_userid, &save_sec_context);
+
+ /*
+ * Row-level security should be disabled in case when foreign-key
+ * relation is queried to check existence of tuples that references
+ * the primary-key being modified.
+ */
+ temp_sec_context = save_sec_context | SECURITY_LOCAL_USERID_CHANGE;
+ if (source_is_pk)
+ temp_sec_context |= SECURITY_ROW_LEVEL_DISABLED;
+
SetUserIdAndSecContext(RelationGetForm(query_rel)->relowner,
! temp_sec_context);
/* Finally we can run the query. */
spi_result = SPI_execute_snapshot(qplan,
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
new file mode 100644
index cf740a9..f1e67a5
*** a/src/backend/utils/cache/plancache.c
--- b/src/backend/utils/cache/plancache.c
***************
*** 53,58 ****
--- 53,59 ----
#include "catalog/namespace.h"
#include "executor/executor.h"
#include "executor/spi.h"
+ #include "miscadmin.h"
#include "nodes/nodeFuncs.h"
#include "optimizer/cost.h"
#include "optimizer/planmain.h"
*************** CheckCachedPlan(CachedPlanSource *planso
*** 795,800 ****
--- 796,811 ----
AcquireExecutorLocks(plan->stmt_list, true);
/*
+ * If plan was constructed with assumption of a particular user-id,
+ * and it is different from the current one, the cached-plan shall
+ * be invalidated to construct suitable query plan.
+ */
+ if (plan->is_valid &&
+ OidIsValid(plan->planUserId) &&
+ plan->planUserId == GetUserId())
+ plan->is_valid = false;
+
+ /*
* If plan was transient, check to see if TransactionXmin has
* advanced, and if so invalidate it.
*/
*************** BuildCachedPlan(CachedPlanSource *planso
*** 847,852 ****
--- 858,865 ----
{
CachedPlan *plan;
List *plist;
+ ListCell *cell;
+ Oid planUserId = InvalidOid;
bool snapshot_set;
bool spi_pushed;
MemoryContext plan_context;
*************** BuildCachedPlan(CachedPlanSource *planso
*** 914,919 ****
--- 927,950 ----
PopActiveSnapshot();
/*
+ * Check whether the generated plan assumes a particular user-id, or not.
+ * In case when a valid user-id is recorded on PlannedStmt->planUserId,
+ * it should be kept and used to validation check of the cached plan
+ * under the "current" user-id.
+ */
+ foreach (cell, plist)
+ {
+ PlannedStmt *pstmt = lfirst(cell);
+
+ if (IsA(pstmt, PlannedStmt) && OidIsValid(pstmt->planUserId))
+ {
+ Assert(!OidIsValid(planUserId) || planUserId == pstmt->planUserId);
+
+ planUserId = pstmt->planUserId;
+ }
+ }
+
+ /*
* Normally we make a dedicated memory context for the CachedPlan and its
* subsidiary data. (It's probably not going to be large, but just in
* case, use the default maxsize parameter. It's transient for the
*************** BuildCachedPlan(CachedPlanSource *planso
*** 956,961 ****
--- 987,993 ----
plan->is_oneshot = plansource->is_oneshot;
plan->is_saved = false;
plan->is_valid = true;
+ plan->planUserId = planUserId;
/* assign generation number to new plan */
plan->generation = ++(plansource->generation);
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
new file mode 100644
index d0acca8..2137e98
*** a/src/backend/utils/cache/relcache.c
--- b/src/backend/utils/cache/relcache.c
***************
*** 50,55 ****
--- 50,56 ----
#include "catalog/pg_opclass.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_rewrite.h"
+ #include "catalog/pg_rowsecurity.h"
#include "catalog/pg_tablespace.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
*************** RelationBuildDesc(Oid targetRelId, bool
*** 932,937 ****
--- 933,943 ----
else
relation->trigdesc = NULL;
+ if (relation->rd_rel->relhasrowsecurity)
+ RelationBuildRowSecurity(relation);
+ else
+ relation->rsdesc = NULL;
+
/*
* if it's an index, initialize index-related information
*/
*************** RelationDestroyRelation(Relation relatio
*** 1840,1845 ****
--- 1846,1853 ----
MemoryContextDelete(relation->rd_indexcxt);
if (relation->rd_rulescxt)
MemoryContextDelete(relation->rd_rulescxt);
+ if (relation->rsdesc)
+ MemoryContextDelete(relation->rsdesc->rscxt);
if (relation->rd_fdwroutine)
pfree(relation->rd_fdwroutine);
pfree(relation);
*************** RelationCacheInitializePhase3(void)
*** 3166,3172 ****
relation->rd_rel->relhastriggers = false;
restart = true;
}
!
/* Release hold on the relation */
RelationDecrementReferenceCount(relation);
--- 3174,3186 ----
relation->rd_rel->relhastriggers = false;
restart = true;
}
! if (relation->rd_rel->relhasrowsecurity && relation->rsdesc == NULL)
! {
! RelationBuildRowSecurity(relation);
! if (relation->rsdesc == NULL)
! relation->rd_rel->relhasrowsecurity = false;
! restart = true;
! }
/* Release hold on the relation */
RelationDecrementReferenceCount(relation);
*************** load_relcache_init_file(bool shared)
*** 4443,4448 ****
--- 4457,4463 ----
rel->rd_rules = NULL;
rel->rd_rulescxt = NULL;
rel->trigdesc = NULL;
+ rel->rsdesc = NULL;
rel->rd_indexprs = NIL;
rel->rd_indpred = NIL;
rel->rd_exclops = NULL;
diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c
new file mode 100644
index 247ad92..9603ef4
*** a/src/bin/pg_dump/common.c
--- b/src/bin/pg_dump/common.c
*************** getSchemaData(Archive *fout, int *numTab
*** 244,249 ****
--- 244,253 ----
write_msg(NULL, "reading rewrite rules\n");
getRules(fout, &numRules);
+ if (g_verbose)
+ write_msg(NULL, "reading row-security policies\n");
+ getRowSecurity(fout, tblinfo, numTables);
+
*numTablesPtr = numTables;
return tblinfo;
}
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
new file mode 100644
index 63a8009..25ff339
*** a/src/bin/pg_dump/pg_backup_archiver.c
--- b/src/bin/pg_dump/pg_backup_archiver.c
*************** _printTocEntry(ArchiveHandle *AH, TocEnt
*** 3133,3138 ****
--- 3133,3139 ----
strcmp(te->desc, "INDEX") == 0 ||
strcmp(te->desc, "RULE") == 0 ||
strcmp(te->desc, "TRIGGER") == 0 ||
+ strcmp(te->desc, "ROW SECURITY") == 0 ||
strcmp(te->desc, "USER MAPPING") == 0)
{
/* these object types don't have separate owners */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
new file mode 100644
index 224e8cb..7d60cf1
*** a/src/bin/pg_dump/pg_dump.c
--- b/src/bin/pg_dump/pg_dump.c
*************** static char *myFormatType(const char *ty
*** 250,255 ****
--- 250,256 ----
static void getBlobs(Archive *fout);
static void dumpBlob(Archive *fout, BlobInfo *binfo);
static int dumpBlobs(Archive *fout, void *arg);
+ static void dumpRowSecurity(Archive *fout, RowSecurityInfo *rsinfo);
static void dumpDatabase(Archive *AH);
static void dumpEncoding(Archive *AH);
static void dumpStdStrings(Archive *AH);
*************** dumpBlobs(Archive *fout, void *arg)
*** 2714,2719 ****
--- 2715,2848 ----
return 1;
}
+ /*
+ * getRowSecurity
+ * get information about every row-security policy on a dumpable table
+ */
+ void
+ getRowSecurity(Archive *fout, TableInfo tblinfo[], int numTables)
+ {
+ PQExpBuffer query = createPQExpBuffer();
+ PGresult *res;
+ RowSecurityInfo *rsinfo;
+ int i_oid;
+ int i_tableoid;
+ int i_rseccmd;
+ int i_rsecqual;
+ int i, j, ntups;
+
+ /* row-security is not supported prior to v9.4 */
+ if (fout->remoteVersion < 90400)
+ return;
+
+ for (i=0; i < numTables; i++)
+ {
+ TableInfo *tbinfo = &tblinfo[i];
+
+ if (!tbinfo->hasrowsec || !tbinfo->dobj.dump)
+ continue;
+
+ if (g_verbose)
+ write_msg(NULL, "reading row-security policy for table \"%s\"\n",
+ tbinfo->dobj.name);
+
+ /*
+ * select table schema to ensure regproc name is qualified if needed
+ */
+ selectSourceSchema(fout, tbinfo->dobj.namespace->dobj.name);
+
+ resetPQExpBuffer(query);
+
+ appendPQExpBuffer(query,
+ "SELECT oid, tableoid, s.rseccmd, "
+ "pg_get_expr(s.rsecqual, s.rsecrelid) AS rsecqual "
+ "FROM pg_catalog.pg_rowsecurity s "
+ "WHERE rsecrelid = '%u'",
+ tbinfo->dobj.catId.oid);
+ res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+
+ ntups = PQntuples(res);
+
+ i_oid = PQfnumber(res, "oid");
+ i_tableoid = PQfnumber(res, "tableoid");
+ i_rseccmd = PQfnumber(res, "rseccmd");
+ i_rsecqual = PQfnumber(res, "rsecqual");
+
+ rsinfo = pg_malloc(ntups * sizeof(RowSecurityInfo));
+ for (j=0; j < ntups; j++)
+ {
+ char namebuf[NAMEDATALEN + 1];
+
+ rsinfo[j].dobj.objType = DO_ROW_SECURITY;
+ rsinfo[j].dobj.catId.tableoid =
+ atooid(PQgetvalue(res, j, i_tableoid));
+ rsinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
+ AssignDumpId(&rsinfo[j].dobj);
+ snprintf(namebuf, sizeof(namebuf), "row-security of %s",
+ tbinfo->rolname);
+ rsinfo[j].dobj.name = namebuf;
+ rsinfo[j].dobj.namespace = tbinfo->dobj.namespace;
+ rsinfo[j].rstable = tbinfo;
+ rsinfo[j].rseccmd = pg_strdup(PQgetvalue(res, j, i_rseccmd));
+ rsinfo[j].rsecqual = pg_strdup(PQgetvalue(res, j, i_rsecqual));
+ }
+ PQclear(res);
+ }
+ destroyPQExpBuffer(query);
+ }
+
+ /*
+ * dumpRowSecurity
+ * dump the definition of the given row-security policy
+ */
+ static void
+ dumpRowSecurity(Archive *fout, RowSecurityInfo *rsinfo)
+ {
+ TableInfo *tbinfo = rsinfo->rstable;
+ PQExpBuffer query;
+ PQExpBuffer delqry;
+ const char *cmd;
+
+ if (dataOnly || !tbinfo->hasrowsec)
+ return;
+
+ query = createPQExpBuffer();
+ delqry = createPQExpBuffer();
+ appendPQExpBuffer(query, "ALTER TABLE %s SET ROW SECURITY ",
+ fmtId(tbinfo->dobj.name));
+ appendPQExpBuffer(delqry, "ALTER TABLE %s RESET ROW SECURITY ",
+ fmtId(tbinfo->dobj.name));
+ if (strcmp(rsinfo->rseccmd, "a") == 0)
+ cmd = "ALL";
+ else if (strcmp(rsinfo->rseccmd, "s") == 0)
+ cmd = "SELECT";
+ else if (strcmp(rsinfo->rseccmd, "i") == 0)
+ cmd = "INSERT";
+ else if (strcmp(rsinfo->rseccmd, "u") == 0)
+ cmd = "UPDATE";
+ else if (strcmp(rsinfo->rseccmd, "d") == 0)
+ cmd = "DELETE";
+ else
+ {
+ write_msg(NULL, "unexpected command type: '%s'\n", rsinfo->rseccmd);
+ exit_nicely(1);
+ }
+ appendPQExpBuffer(query, "FOR %s TO %s;\n", cmd, rsinfo->rsecqual);
+ appendPQExpBuffer(delqry, "FOR %s;\n", cmd);
+
+ ArchiveEntry(fout, rsinfo->dobj.catId, rsinfo->dobj.dumpId,
+ rsinfo->dobj.name,
+ rsinfo->dobj.namespace->dobj.name,
+ NULL,
+ tbinfo->rolname, false,
+ "ROW SECURITY", SECTION_POST_DATA,
+ query->data, delqry->data, NULL,
+ NULL, 0,
+ NULL, NULL);
+
+ destroyPQExpBuffer(query);
+ }
+
static void
binary_upgrade_set_type_oids_by_type_oid(Archive *fout,
PQExpBuffer upgrade_buffer,
*************** getTables(Archive *fout, int *numTables)
*** 4242,4247 ****
--- 4371,4377 ----
int i_relhastriggers;
int i_relhasindex;
int i_relhasrules;
+ int i_relhasrowsec;
int i_relhasoids;
int i_relfrozenxid;
int i_toastoid;
*************** getTables(Archive *fout, int *numTables)
*** 4293,4302 ****
"(%s c.relowner) AS rolname, "
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
! "c.relfrozenxid, tc.oid AS toid, "
! "tc.relfrozenxid AS tfrozenxid, "
! "c.relpersistence, c.relispopulated, "
! "c.relreplident, c.relpages, "
"CASE WHEN c.reloftype <> 0 THEN c.reloftype::pg_catalog.regtype ELSE NULL END AS reloftype, "
"d.refobjid AS owning_tab, "
"d.refobjsubid AS owning_col, "
--- 4423,4429 ----
"(%s c.relowner) AS rolname, "
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
! "c.relhasrowsecurity, "
"CASE WHEN c.reloftype <> 0 THEN c.reloftype::pg_catalog.regtype ELSE NULL END AS reloftype, "
"d.refobjid AS owning_tab, "
"d.refobjsubid AS owning_col, "
*************** getTables(Archive *fout, int *numTables)
*** 4332,4337 ****
--- 4459,4465 ----
"(%s c.relowner) AS rolname, "
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"c.relfrozenxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"c.relpersistence, c.relispopulated, "
*************** getTables(Archive *fout, int *numTables)
*** 4371,4376 ****
--- 4499,4505 ----
"(%s c.relowner) AS rolname, "
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"c.relfrozenxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"c.relpersistence, 't' as relispopulated, "
*************** getTables(Archive *fout, int *numTables)
*** 4408,4413 ****
--- 4537,4543 ----
"(%s c.relowner) AS rolname, "
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"c.relfrozenxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"'p' AS relpersistence, 't' as relispopulated, "
*************** getTables(Archive *fout, int *numTables)
*** 4444,4449 ****
--- 4574,4580 ----
"(%s c.relowner) AS rolname, "
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"c.relfrozenxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"'p' AS relpersistence, 't' as relispopulated, "
*************** getTables(Archive *fout, int *numTables)
*** 4480,4485 ****
--- 4611,4617 ----
"(%s c.relowner) AS rolname, "
"c.relchecks, (c.reltriggers <> 0) AS relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"c.relfrozenxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"'p' AS relpersistence, 't' as relispopulated, "
*************** getTables(Archive *fout, int *numTables)
*** 4516,4521 ****
--- 4648,4654 ----
"(%s relowner) AS rolname, "
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"0 AS relfrozenxid, "
"0 AS toid, "
"0 AS tfrozenxid, "
*************** getTables(Archive *fout, int *numTables)
*** 4552,4557 ****
--- 4685,4691 ----
"(%s relowner) AS rolname, "
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"0 AS relfrozenxid, "
"0 AS toid, "
"0 AS tfrozenxid, "
*************** getTables(Archive *fout, int *numTables)
*** 4584,4589 ****
--- 4718,4724 ----
"(%s relowner) AS rolname, "
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"0 AS relfrozenxid, "
"0 AS toid, "
"0 AS tfrozenxid, "
*************** getTables(Archive *fout, int *numTables)
*** 4611,4616 ****
--- 4746,4752 ----
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, "
"'t'::bool AS relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"0 AS relfrozenxid, "
"0 AS toid, "
"0 AS tfrozenxid, "
*************** getTables(Archive *fout, int *numTables)
*** 4648,4653 ****
--- 4784,4790 ----
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, "
"'t'::bool AS relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"0 as relfrozenxid, "
"0 AS toid, "
"0 AS tfrozenxid, "
*************** getTables(Archive *fout, int *numTables)
*** 4695,4700 ****
--- 4832,4838 ----
i_relhastriggers = PQfnumber(res, "relhastriggers");
i_relhasindex = PQfnumber(res, "relhasindex");
i_relhasrules = PQfnumber(res, "relhasrules");
+ i_relhasrowsec = PQfnumber(res, "relhasrowsecurity");
i_relhasoids = PQfnumber(res, "relhasoids");
i_relfrozenxid = PQfnumber(res, "relfrozenxid");
i_toastoid = PQfnumber(res, "toid");
*************** getTables(Archive *fout, int *numTables)
*** 4744,4749 ****
--- 4882,4888 ----
tblinfo[i].hasindex = (strcmp(PQgetvalue(res, i, i_relhasindex), "t") == 0);
tblinfo[i].hasrules = (strcmp(PQgetvalue(res, i, i_relhasrules), "t") == 0);
tblinfo[i].hastriggers = (strcmp(PQgetvalue(res, i, i_relhastriggers), "t") == 0);
+ tblinfo[i].hasrowsec = (strcmp(PQgetvalue(res, i, i_relhasrowsec), "t") == 0);
tblinfo[i].hasoids = (strcmp(PQgetvalue(res, i, i_relhasoids), "t") == 0);
tblinfo[i].relispopulated = (strcmp(PQgetvalue(res, i, i_relispopulated), "t") == 0);
tblinfo[i].relreplident = *(PQgetvalue(res, i, i_relreplident));
*************** dumpDumpableObject(Archive *fout, Dumpab
*** 7861,7866 ****
--- 8000,8008 ----
NULL, 0,
dumpBlobs, NULL);
break;
+ case DO_ROW_SECURITY:
+ dumpRowSecurity(fout, (RowSecurityInfo *) dobj);
+ break;
case DO_PRE_DATA_BOUNDARY:
case DO_POST_DATA_BOUNDARY:
/* never dumped, nothing to do */
*************** addBoundaryDependencies(DumpableObject *
*** 15118,15123 ****
--- 15260,15266 ----
case DO_TRIGGER:
case DO_EVENT_TRIGGER:
case DO_DEFAULT_ACL:
+ case DO_ROW_SECURITY:
/* Post-data objects: must come after the post-data boundary */
addObjectDependency(dobj, postDataBound->dumpId);
break;
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
new file mode 100644
index 915e82c..72c0433
*** a/src/bin/pg_dump/pg_dump.h
--- b/src/bin/pg_dump/pg_dump.h
*************** typedef enum
*** 111,117 ****
DO_PRE_DATA_BOUNDARY,
DO_POST_DATA_BOUNDARY,
DO_EVENT_TRIGGER,
! DO_REFRESH_MATVIEW
} DumpableObjectType;
typedef struct _dumpableObject
--- 111,118 ----
DO_PRE_DATA_BOUNDARY,
DO_POST_DATA_BOUNDARY,
DO_EVENT_TRIGGER,
! DO_REFRESH_MATVIEW,
! DO_ROW_SECURITY,
} DumpableObjectType;
typedef struct _dumpableObject
*************** typedef struct _tableInfo
*** 245,250 ****
--- 246,252 ----
bool hasindex; /* does it have any indexes? */
bool hasrules; /* does it have any rules? */
bool hastriggers; /* does it have any triggers? */
+ bool hasrowsec; /* does it have any row-security policy? */
bool hasoids; /* does it have OIDs? */
uint32 frozenxid; /* for restore frozen xid */
Oid toast_oid; /* for restore toast frozen xid */
*************** typedef struct _blobInfo
*** 484,489 ****
--- 486,499 ----
char *blobacl;
} BlobInfo;
+ typedef struct _rowSecurityInfo
+ {
+ DumpableObject dobj;
+ TableInfo *rstable;
+ char *rseccmd;
+ char *rsecqual;
+ } RowSecurityInfo;
+
/* global decls */
extern bool force_quotes; /* double-quotes for identifiers flag */
extern bool g_verbose; /* verbose flag */
*************** extern DefaultACLInfo *getDefaultACLs(Ar
*** 575,579 ****
--- 585,590 ----
extern void getExtensionMembership(Archive *fout, ExtensionInfo extinfo[],
int numExtensions);
extern EventTriggerInfo *getEventTriggers(Archive *fout, int *numEventTriggers);
+ extern void getRowSecurity(Archive *fout, TableInfo tblinfo[], int numTables);
#endif /* PG_DUMP_H */
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
new file mode 100644
index 141e713..5c5777c
*** a/src/bin/pg_dump/pg_dump_sort.c
--- b/src/bin/pg_dump/pg_dump_sort.c
*************** describeDumpableObject(DumpableObject *o
*** 1342,1347 ****
--- 1342,1352 ----
"BLOB DATA (ID %d)",
obj->dumpId);
return;
+ case DO_ROW_SECURITY:
+ snprintf(buf, bufsize,
+ "ROW-SECURITY POLICY (ID %d OID %u)",
+ obj->dumpId, obj->catId.oid);
+ return;
case DO_PRE_DATA_BOUNDARY:
snprintf(buf, bufsize,
"PRE-DATA BOUNDARY (ID %d)",
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
new file mode 100644
index 96322ca..89e1c09
*** a/src/bin/psql/describe.c
--- b/src/bin/psql/describe.c
*************** listTables(const char *tabtypes, const c
*** 2752,2757 ****
--- 2752,2761 ----
appendPQExpBuffer(&buf,
",\n pg_catalog.obj_description(c.oid, 'pg_class') as \"%s\"",
gettext_noop("Description"));
+ if (pset.sversion >= 90300)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_get_expr(rs.rsecqual, c.oid) as \"%s\"",
+ gettext_noop("Row-security"));
}
appendPQExpBufferStr(&buf,
*************** listTables(const char *tabtypes, const c
*** 2761,2766 ****
--- 2765,2773 ----
appendPQExpBufferStr(&buf,
"\n LEFT JOIN pg_catalog.pg_index i ON i.indexrelid = c.oid"
"\n LEFT JOIN pg_catalog.pg_class c2 ON i.indrelid = c2.oid");
+ if (verbose && pset.sversion >= 90300)
+ appendPQExpBuffer(&buf,
+ "\n LEFT JOIN pg_rowsecurity rs ON rs.rsecrelid = c.oid");
appendPQExpBufferStr(&buf, "\nWHERE c.relkind IN (");
if (showTables)
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
new file mode 100644
index 3aefbb5..c96e8d4
*** a/src/include/catalog/dependency.h
--- b/src/include/catalog/dependency.h
*************** typedef enum ObjectClass
*** 147,152 ****
--- 147,153 ----
OCLASS_DEFACL, /* pg_default_acl */
OCLASS_EXTENSION, /* pg_extension */
OCLASS_EVENT_TRIGGER, /* pg_event_trigger */
+ OCLASS_ROWSECURITY, /* pg_rowsecurity */
MAX_OCLASS /* MUST BE LAST */
} ObjectClass;
diff --git a/src/include/catalog/indexing.h b/src/include/catalog/indexing.h
new file mode 100644
index 4860e98..04dbeca
*** a/src/include/catalog/indexing.h
--- b/src/include/catalog/indexing.h
*************** DECLARE_UNIQUE_INDEX(pg_extension_name_i
*** 313,318 ****
--- 313,323 ----
DECLARE_UNIQUE_INDEX(pg_range_rngtypid_index, 3542, on pg_range using btree(rngtypid oid_ops));
#define RangeTypidIndexId 3542
+ DECLARE_UNIQUE_INDEX(pg_rowsecurity_oid_index, 5001, on pg_rowsecurity using btree(oid oid_ops));
+ #define RowSecurityOidIndexId 5001
+ DECLARE_UNIQUE_INDEX(pg_rowsecurity_relid_index, 5002, on pg_rowsecurity using btree(rsecrelid oid_ops, rseccmd char_ops));
+ #define RowSecurityRelidIndexId 5002
+
/* last step of initialization script: build the indexes declared above */
BUILD_INDICES
diff --git a/src/include/catalog/pg_class.h b/src/include/catalog/pg_class.h
new file mode 100644
index a1fee11..d937924
*** a/src/include/catalog/pg_class.h
--- b/src/include/catalog/pg_class.h
*************** CATALOG(pg_class,1259) BKI_BOOTSTRAP BKI
*** 65,70 ****
--- 65,71 ----
bool relhasrules; /* has (or has had) any rules */
bool relhastriggers; /* has (or has had) any TRIGGERs */
bool relhassubclass; /* has (or has had) derived classes */
+ bool relhasrowsecurity; /* has (or has had) row-security policy */
bool relispopulated; /* matview currently holds query results */
char relreplident; /* see REPLICA_IDENTITY_xxx constants */
TransactionId relfrozenxid; /* all Xids < this are frozen in this rel */
*************** typedef FormData_pg_class *Form_pg_class
*** 94,100 ****
* ----------------
*/
! #define Natts_pg_class 29
#define Anum_pg_class_relname 1
#define Anum_pg_class_relnamespace 2
#define Anum_pg_class_reltype 3
--- 95,101 ----
* ----------------
*/
! #define Natts_pg_class 30
#define Anum_pg_class_relname 1
#define Anum_pg_class_relnamespace 2
#define Anum_pg_class_reltype 3
*************** typedef FormData_pg_class *Form_pg_class
*** 118,129 ****
#define Anum_pg_class_relhasrules 21
#define Anum_pg_class_relhastriggers 22
#define Anum_pg_class_relhassubclass 23
! #define Anum_pg_class_relispopulated 24
! #define Anum_pg_class_relreplident 25
! #define Anum_pg_class_relfrozenxid 26
! #define Anum_pg_class_relminmxid 27
! #define Anum_pg_class_relacl 28
! #define Anum_pg_class_reloptions 29
/* ----------------
* initial contents of pg_class
--- 119,131 ----
#define Anum_pg_class_relhasrules 21
#define Anum_pg_class_relhastriggers 22
#define Anum_pg_class_relhassubclass 23
! #define Anum_pg_class_relhasrowsecurity 24
! #define Anum_pg_class_relispopulated 25
! #define Anum_pg_class_relreplident 26
! #define Anum_pg_class_relfrozenxid 27
! #define Anum_pg_class_relminmxid 28
! #define Anum_pg_class_relacl 29
! #define Anum_pg_class_reloptions 30
/* ----------------
* initial contents of pg_class
*************** typedef FormData_pg_class *Form_pg_class
*** 138,150 ****
* Note: "3" in the relfrozenxid column stands for FirstNormalTransactionId;
* similarly, "1" in relminmxid stands for FirstMultiXactId
*/
! DATA(insert OID = 1247 ( pg_type PGNSP 71 0 PGUID 0 0 0 0 0 0 0 f f p r 30 0 t f f f f t n 3 1 _null_ _null_ ));
DESCR("");
! DATA(insert OID = 1249 ( pg_attribute PGNSP 75 0 PGUID 0 0 0 0 0 0 0 f f p r 21 0 f f f f f t n 3 1 _null_ _null_ ));
DESCR("");
! DATA(insert OID = 1255 ( pg_proc PGNSP 81 0 PGUID 0 0 0 0 0 0 0 f f p r 27 0 t f f f f t n 3 1 _null_ _null_ ));
DESCR("");
! DATA(insert OID = 1259 ( pg_class PGNSP 83 0 PGUID 0 0 0 0 0 0 0 f f p r 29 0 t f f f f t n 3 1 _null_ _null_ ));
DESCR("");
--- 140,153 ----
* Note: "3" in the relfrozenxid column stands for FirstNormalTransactionId;
* similarly, "1" in relminmxid stands for FirstMultiXactId
*/
!
! DATA(insert OID = 1247 ( pg_type PGNSP 71 0 PGUID 0 0 0 0 0 0 0 f f p r 29 0 t f f f f f t n 3 1 _null_ _null_ ));
DESCR("");
! DATA(insert OID = 1249 ( pg_attribute PGNSP 75 0 PGUID 0 0 0 0 0 0 0 f f p r 21 0 f f f f f f t n 3 1 _null_ _null_ ));
DESCR("");
! DATA(insert OID = 1255 ( pg_proc PGNSP 81 0 PGUID 0 0 0 0 0 0 0 f f p r 27 0 t f f f f f t n 3 1 _null_ _null_ ));
DESCR("");
! DATA(insert OID = 1259 ( pg_class PGNSP 83 0 PGUID 0 0 0 0 0 0 0 f f p r 30 0 t f f f f f t n 3 1 _null_ _null_ ));
DESCR("");
diff --git a/src/include/catalog/pg_rowsecurity.h b/src/include/catalog/pg_rowsecurity.h
new file mode 100644
index ...798d556
*** a/src/include/catalog/pg_rowsecurity.h
--- b/src/include/catalog/pg_rowsecurity.h
***************
*** 0 ****
--- 1,76 ----
+ /*
+ * pg_rowsecurity.h
+ * definition of the system catalog for row-security policy (pg_rowsecurity)
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ */
+ #ifndef PG_ROWSECURITY_H
+ #define PG_ROWSECURITY_H
+
+ #include "catalog/genbki.h"
+ #include "nodes/primnodes.h"
+ #include "utils/memutils.h"
+ #include "utils/relcache.h"
+
+ /* ----------------
+ * pg_rowlevelsec definition. cpp turns this into
+ * typedef struct FormData_pg_rowlevelsec
+ * ----------------
+ */
+ #define RowSecurityRelationId 5000
+
+ CATALOG(pg_rowsecurity,5000)
+ {
+ /* Oid of the relation that has row-security policy */
+ Oid rsecrelid;
+
+ /* One of ROWSECURITY_CMD_* below */
+ char rseccmd;
+ #ifdef CATALOG_VARLEN
+ pg_node_tree rsecqual;
+ #endif
+ } FormData_pg_rowsecurity;
+
+ /* ----------------
+ * Form_pg_rowlevelsec corresponds to a pointer to a row with
+ * the format of pg_rowlevelsec relation.
+ * ----------------
+ */
+ typedef FormData_pg_rowsecurity *Form_pg_rowsecurity;
+
+ /* ----------------
+ * compiler constants for pg_rowlevelsec
+ * ----------------
+ */
+ #define Natts_pg_rowsecurity 3
+ #define Anum_pg_rowsecurity_rsecrelid 1
+ #define Anum_pg_rowsecurity_rseccmd 2
+ #define Anum_pg_rowsecurity_rsecqual 3
+
+ #define ROWSECURITY_CMD_ALL 'a'
+ #define ROWSECURITY_CMD_SELECT 's'
+ #define ROWSECURITY_CMD_INSERT 'i'
+ #define ROWSECURITY_CMD_UPDATE 'u'
+ #define ROWSECURITY_CMD_DELETE 'd'
+
+ typedef struct
+ {
+ Oid rsecid;
+ Expr *qual;
+ bool hassublinks;
+ } RowSecurityEntry;
+
+ typedef struct
+ {
+ MemoryContext rscxt;
+ RowSecurityEntry rsall; /* row-security policy for ALL */
+ } RowSecurityDesc;
+
+ extern void RelationBuildRowSecurity(Relation relation);
+ extern void ATExecSetRowSecurity(Relation relation,
+ const char *cmdname, Node *clause);
+ extern void RemoveRowSecurityById(Oid relationId);
+
+ #endif /* PG_ROWSECURITY_H */
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
new file mode 100644
index 98ca553..8d76387
*** a/src/include/miscadmin.h
--- b/src/include/miscadmin.h
*************** extern int trace_recovery(int trace_leve
*** 272,277 ****
--- 272,278 ----
/* flags to be OR'd to form sec_context */
#define SECURITY_LOCAL_USERID_CHANGE 0x0001
#define SECURITY_RESTRICTED_OPERATION 0x0002
+ #define SECURITY_ROW_LEVEL_DISABLED 0x0004
extern char *DatabasePath;
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
new file mode 100644
index 5a40347..4133b8c
*** a/src/include/nodes/execnodes.h
--- b/src/include/nodes/execnodes.h
*************** typedef struct JunkFilter
*** 308,313 ****
--- 308,315 ----
* ConstraintExprs array of constraint-checking expr states
* junkFilter for removing junk attributes from tuples
* projectReturning for computing a RETURNING list
+ * rowSecurity for row-security checks
+ * rowSecParams param-list if row-security has SubLink
* ----------------
*/
typedef struct ResultRelInfo
*************** typedef struct ResultRelInfo
*** 329,334 ****
--- 331,338 ----
List **ri_ConstraintExprs;
JunkFilter *ri_junkFilter;
ProjectionInfo *ri_projectReturning;
+ Node *ri_rowSecurity;
+ List *ri_rowSecParams;
} ResultRelInfo;
/* ----------------
diff --git a/src/include/nodes/nodeFuncs.h b/src/include/nodes/nodeFuncs.h
new file mode 100644
index fe7cfd3..23c3553
*** a/src/include/nodes/nodeFuncs.h
--- b/src/include/nodes/nodeFuncs.h
***************
*** 24,29 ****
--- 24,30 ----
#define QTW_IGNORE_RANGE_TABLE 0x08 /* skip rangetable entirely */
#define QTW_EXAMINE_RTES 0x10 /* examine RTEs */
#define QTW_DONT_COPY_QUERY 0x20 /* do not copy top Query */
+ #define QTW_IGNORE_RETURNING 0x40 /* skip returning clause */
extern Oid exprType(const Node *expr);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
new file mode 100644
index 6a5555f..8163708
*** a/src/include/nodes/parsenodes.h
--- b/src/include/nodes/parsenodes.h
*************** typedef enum QuerySource
*** 31,37 ****
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 */
} QuerySource;
/* Sort ordering options for ORDER BY and CREATE INDEX */
--- 31,38 ----
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_ROW_SECURITY, /* added by row-security */
} QuerySource;
/* Sort ordering options for ORDER BY and CREATE INDEX */
*************** typedef struct Query
*** 112,118 ****
int resultRelation; /* rtable index of target relation for
* INSERT/UPDATE/DELETE; 0 for SELECT */
!
bool hasAggs; /* has aggregates in tlist or havingQual */
bool hasWindowFuncs; /* has window functions in tlist */
bool hasSubLinks; /* has subquery SubLink */
--- 113,121 ----
int resultRelation; /* rtable index of target relation for
* INSERT/UPDATE/DELETE; 0 for SELECT */
! int sourceRelation; /* rtable index of source relation for
! * UPDATE/DELETE, if not identical with
! * resultRelation; 0 for elsewhere */
bool hasAggs; /* has aggregates in tlist or havingQual */
bool hasWindowFuncs; /* has window functions in tlist */
bool hasSubLinks; /* has subquery SubLink */
*************** typedef struct RangeTblEntry
*** 739,744 ****
--- 742,752 ----
*/
Query *subquery; /* the sub-query */
bool security_barrier; /* is from security_barrier view? */
+ Oid rowsec_relid; /* OID of the original relation, if this
+ * sub-query originated from row-security
+ * policy on the relation. Elsewhere, it
+ * should be InvalidOid.
+ */
/*
* Fields valid for a join RTE (else NULL/zero):
*************** typedef enum AlterTableType
*** 1314,1319 ****
--- 1322,1329 ----
AT_AddOf, /* OF <type_name> */
AT_DropOf, /* NOT OF */
AT_ReplicaIdentity, /* REPLICA IDENTITY */
+ AT_SetRowSecurity, /* SET ROW SECURITY (...) */
+ AT_ResetRowSecurity, /* RESET ROW SECURITY */
AT_GenericOptions /* OPTIONS (...) */
} AlterTableType;
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
new file mode 100644
index 101e22c..e675e87
*** a/src/include/nodes/plannodes.h
--- b/src/include/nodes/plannodes.h
*************** typedef struct PlannedStmt
*** 67,72 ****
--- 67,74 ----
List *invalItems; /* other dependencies, as PlanInvalItems */
int nParamExec; /* number of PARAM_EXEC Params used */
+
+ Oid planUserId; /* user-id this plan assumed, or InvalidOid */
} PlannedStmt;
/* macro for fetching the Plan associated with a SubPlan node */
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
new file mode 100644
index 6d7b594..ee454fa
*** a/src/include/nodes/relation.h
--- b/src/include/nodes/relation.h
*************** typedef struct PlannerGlobal
*** 98,103 ****
--- 98,105 ----
Index lastRowMarkId; /* highest PlanRowMark ID assigned */
bool transientPlan; /* redo plan when TransactionXmin changes? */
+
+ Oid planUserId; /* User-Id to be assumed on this plan */
} PlannerGlobal;
/* macro for fetching the Plan associated with a SubPlan node */
*************** typedef struct AppendRelInfo
*** 1435,1440 ****
--- 1437,1446 ----
*/
Index parent_relid; /* RT index of append parent rel */
Index child_relid; /* RT index of append child rel */
+ Index child_result; /* RT index of append child rel's source,
+ * if source of result relation is not
+ * identical. Elsewhere, 0.
+ */
/*
* For an inheritance appendrel, the parent and child are both regular
diff --git a/src/include/optimizer/rowsecurity.h b/src/include/optimizer/rowsecurity.h
new file mode 100644
index ...ff4dd9a
*** a/src/include/optimizer/rowsecurity.h
--- b/src/include/optimizer/rowsecurity.h
***************
*** 0 ****
--- 1,27 ----
+ /* -------------------------------------------------------------------------
+ *
+ * rowsecurity.h
+ * prototypes for optimizer/rowsecurity.c
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * -------------------------------------------------------------------------
+ */
+ #ifndef ROWSECURITY_H
+ #define ROWSECURITY_H
+
+ #include "nodes/execnodes.h"
+ #include "nodes/parsenodes.h"
+ #include "nodes/relation.h"
+ #include "utils/rel.h"
+
+ typedef List *(*row_security_policy_hook_type)(CmdType cmdtype,
+ Relation relation);
+ extern PGDLLIMPORT row_security_policy_hook_type row_security_policy_hook;
+
+ extern bool copy_row_security_policy(CopyStmt *stmt,
+ Relation relation, List *attnums);
+ extern void apply_row_security_policy(PlannerInfo *root);
+
+ #endif /* ROWSECURITY_H */
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
new file mode 100644
index bea3b07..3910853
*** a/src/include/parser/parse_node.h
--- b/src/include/parser/parse_node.h
*************** typedef enum ParseExprKind
*** 63,69 ****
EXPR_KIND_INDEX_PREDICATE, /* index predicate */
EXPR_KIND_ALTER_COL_TRANSFORM, /* transform expr in ALTER COLUMN TYPE */
EXPR_KIND_EXECUTE_PARAMETER, /* parameter value in EXECUTE */
! EXPR_KIND_TRIGGER_WHEN /* WHEN condition in CREATE TRIGGER */
} ParseExprKind;
--- 63,70 ----
EXPR_KIND_INDEX_PREDICATE, /* index predicate */
EXPR_KIND_ALTER_COL_TRANSFORM, /* transform expr in ALTER COLUMN TYPE */
EXPR_KIND_EXECUTE_PARAMETER, /* parameter value in EXECUTE */
! EXPR_KIND_TRIGGER_WHEN, /* WHEN condition in CREATE TRIGGER */
! EXPR_KIND_ROW_SECURITY, /* ROW SECURITY policy for a table */
} ParseExprKind;
diff --git a/src/include/rewrite/rewriteHandler.h b/src/include/rewrite/rewriteHandler.h
new file mode 100644
index c959590..ef10f56
*** a/src/include/rewrite/rewriteHandler.h
--- b/src/include/rewrite/rewriteHandler.h
***************
*** 18,23 ****
--- 18,24 ----
#include "nodes/parsenodes.h"
extern List *QueryRewrite(Query *parsetree);
+ extern void QueryRewriteExpr(Node *node, List *activeRIRs);
extern void AcquireRewriteLocks(Query *parsetree, bool forUpdatePushedDown);
extern Node *build_column_default(Relation rel, int attrno);
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
new file mode 100644
index 72f8491..35819fa
*** a/src/include/utils/plancache.h
--- b/src/include/utils/plancache.h
*************** typedef struct CachedPlan
*** 128,133 ****
--- 128,135 ----
bool is_oneshot; /* is it a "oneshot" plan? */
bool is_saved; /* is CachedPlan in a long-lived context? */
bool is_valid; /* is the stmt_list currently valid? */
+ Oid planUserId; /* is user-id that is assumed on this cached
+ plan, or InvalidOid if portable for anybody */
TransactionId saved_xmin; /* if valid, replan when TransactionXmin
* changes from this value */
int generation; /* parent's generation number for this plan */
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
new file mode 100644
index 21d5871..9a324ab
*** a/src/include/utils/rel.h
--- b/src/include/utils/rel.h
***************
*** 18,23 ****
--- 18,24 ----
#include "catalog/pg_am.h"
#include "catalog/pg_class.h"
#include "catalog/pg_index.h"
+ #include "catalog/pg_rowsecurity.h"
#include "fmgr.h"
#include "nodes/bitmapset.h"
#include "rewrite/prs2lock.h"
*************** typedef struct RelationData
*** 109,114 ****
--- 110,116 ----
RuleLock *rd_rules; /* rewrite rules */
MemoryContext rd_rulescxt; /* private memory cxt for rd_rules, if any */
TriggerDesc *trigdesc; /* Trigger info, or NULL if rel has none */
+ RowSecurityDesc *rsdesc; /* Row-security policy, or NULL */
/*
* The index chosen as the relation's replication identity or
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
new file mode 100644
index ...04591df
*** a/src/test/regress/expected/rowsecurity.out
--- b/src/test/regress/expected/rowsecurity.out
***************
*** 0 ****
--- 1,950 ----
+ --
+ -- Test of Row-level security feature
+ --
+ -- Clean up in case a prior regression run failed
+ -- Suppress NOTICE messages when users/groups don't exist
+ SET client_min_messages TO 'warning';
+ DROP USER IF EXISTS rls_regress_user0;
+ DROP USER IF EXISTS rls_regress_user1;
+ DROP USER IF EXISTS rls_regress_user2;
+ DROP SCHEMA IF EXISTS rls_regress_schema CASCADE;
+ RESET client_min_messages;
+ -- initial setup
+ CREATE USER rls_regress_user0;
+ CREATE USER rls_regress_user1;
+ CREATE USER rls_regress_user2;
+ CREATE SCHEMA rls_regress_schema;
+ GRANT ALL ON SCHEMA rls_regress_schema TO public;
+ SET search_path = rls_regress_schema;
+ -- setup of malicious function
+ CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool
+ COST 0.0000001 LANGUAGE plpgsql
+ AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END';
+ GRANT EXECUTE ON FUNCTION f_leak(text) TO public;
+ -- BASIC Row-Level Security Scenario
+ SET SESSION AUTHORIZATION rls_regress_user0;
+ CREATE TABLE uaccount (
+ pguser name primary key,
+ seclv int
+ );
+ INSERT INTO uaccount VALUES
+ ('rls_regress_user0', 99),
+ ('rls_regress_user1', 1),
+ ('rls_regress_user2', 2),
+ ('rls_regress_user3', 3);
+ GRANT SELECT ON uaccount TO public;
+ CREATE TABLE category (
+ cid int primary key,
+ cname text
+ );
+ GRANT ALL ON category TO public;
+ INSERT INTO category VALUES
+ (11, 'novel'),
+ (22, 'science fiction'),
+ (33, 'technology'),
+ (44, 'manga');
+ CREATE TABLE document (
+ did int primary key,
+ cid int references category(cid),
+ dlevel int not null,
+ dauthor name,
+ dtitle text
+ );
+ GRANT ALL ON document TO public;
+ INSERT INTO document VALUES
+ ( 1, 11, 1, 'rls_regress_user1', 'my first novel'),
+ ( 2, 11, 2, 'rls_regress_user1', 'my second novel'),
+ ( 3, 22, 2, 'rls_regress_user1', 'my science fiction'),
+ ( 4, 44, 1, 'rls_regress_user1', 'my first manga'),
+ ( 5, 44, 2, 'rls_regress_user1', 'my second manga'),
+ ( 6, 22, 1, 'rls_regress_user2', 'great science fiction'),
+ ( 7, 33, 2, 'rls_regress_user2', 'great technology book'),
+ ( 8, 44, 1, 'rls_regress_user2', 'great manga');
+ -- user's security level must higher than or equal to document's one
+ ALTER TABLE document SET ROW SECURITY FOR ALL
+ TO (dlevel <= (SELECT seclv FROM uaccount WHERE pguser = current_user));
+ -- viewpoint from rls_regress_user1
+ SET SESSION AUTHORIZATION rls_regress_user1;
+ SELECT * FROM document WHERE f_leak(dtitle);
+ NOTICE: f_leak => my first novel
+ NOTICE: f_leak => my first manga
+ NOTICE: f_leak => great science fiction
+ NOTICE: f_leak => great manga
+ did | cid | dlevel | dauthor | dtitle
+ -----+-----+--------+-------------------+-----------------------
+ 1 | 11 | 1 | rls_regress_user1 | my first novel
+ 4 | 44 | 1 | rls_regress_user1 | my first manga
+ 6 | 22 | 1 | rls_regress_user2 | great science fiction
+ 8 | 44 | 1 | rls_regress_user2 | great manga
+ (4 rows)
+
+ SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+ NOTICE: f_leak => my first novel
+ NOTICE: f_leak => my first manga
+ NOTICE: f_leak => great science fiction
+ NOTICE: f_leak => great manga
+ cid | did | dlevel | dauthor | dtitle | cname
+ -----+-----+--------+-------------------+-----------------------+-----------------
+ 11 | 1 | 1 | rls_regress_user1 | my first novel | novel
+ 22 | 6 | 1 | rls_regress_user2 | great science fiction | science fiction
+ 44 | 8 | 1 | rls_regress_user2 | great manga | manga
+ 44 | 4 | 1 | rls_regress_user1 | my first manga | manga
+ (4 rows)
+
+ -- viewpoint from rls_regress_user2
+ SET SESSION AUTHORIZATION rls_regress_user2;
+ SELECT * FROM document WHERE f_leak(dtitle);
+ NOTICE: f_leak => my first novel
+ NOTICE: f_leak => my second novel
+ NOTICE: f_leak => my science fiction
+ NOTICE: f_leak => my first manga
+ NOTICE: f_leak => my second manga
+ NOTICE: f_leak => great science fiction
+ NOTICE: f_leak => great technology book
+ NOTICE: f_leak => great manga
+ did | cid | dlevel | dauthor | dtitle
+ -----+-----+--------+-------------------+-----------------------
+ 1 | 11 | 1 | rls_regress_user1 | my first novel
+ 2 | 11 | 2 | rls_regress_user1 | my second novel
+ 3 | 22 | 2 | rls_regress_user1 | my science fiction
+ 4 | 44 | 1 | rls_regress_user1 | my first manga
+ 5 | 44 | 2 | rls_regress_user1 | my second manga
+ 6 | 22 | 1 | rls_regress_user2 | great science fiction
+ 7 | 33 | 2 | rls_regress_user2 | great technology book
+ 8 | 44 | 1 | rls_regress_user2 | great manga
+ (8 rows)
+
+ SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+ NOTICE: f_leak => my first novel
+ NOTICE: f_leak => my second novel
+ NOTICE: f_leak => my science fiction
+ NOTICE: f_leak => my first manga
+ NOTICE: f_leak => my second manga
+ NOTICE: f_leak => great science fiction
+ NOTICE: f_leak => great technology book
+ NOTICE: f_leak => great manga
+ cid | did | dlevel | dauthor | dtitle | cname
+ -----+-----+--------+-------------------+-----------------------+-----------------
+ 11 | 2 | 2 | rls_regress_user1 | my second novel | novel
+ 11 | 1 | 1 | rls_regress_user1 | my first novel | novel
+ 22 | 6 | 1 | rls_regress_user2 | great science fiction | science fiction
+ 22 | 3 | 2 | rls_regress_user1 | my science fiction | science fiction
+ 33 | 7 | 2 | rls_regress_user2 | great technology book | technology
+ 44 | 8 | 1 | rls_regress_user2 | great manga | manga
+ 44 | 5 | 2 | rls_regress_user1 | my second manga | manga
+ 44 | 4 | 1 | rls_regress_user1 | my first manga | manga
+ (8 rows)
+
+ EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+ QUERY PLAN
+ ----------------------------------------------------------
+ Subquery Scan on document
+ Filter: f_leak(document.dtitle)
+ -> Seq Scan on document document_1
+ Filter: (dlevel <= $0)
+ InitPlan 1 (returns $0)
+ -> Index Scan using uaccount_pkey on uaccount
+ Index Cond: (pguser = "current_user"())
+ (7 rows)
+
+ EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+ QUERY PLAN
+ ----------------------------------------------------------------------
+ Hash Join
+ Hash Cond: (category.cid = document.cid)
+ -> Seq Scan on category
+ -> Hash
+ -> Subquery Scan on document
+ Filter: f_leak(document.dtitle)
+ -> Seq Scan on document document_1
+ Filter: (dlevel <= $0)
+ InitPlan 1 (returns $0)
+ -> Index Scan using uaccount_pkey on uaccount
+ Index Cond: (pguser = "current_user"())
+ (11 rows)
+
+ -- only owner can change row-level security
+ ALTER TABLE document SET ROW SECURITY FOR ALL TO (true); -- fail
+ ERROR: must be owner of relation document
+ ALTER TABLE document RESET ROW SECURITY FOR ALL; -- fail
+ ERROR: must be owner of relation document
+ SET SESSION AUTHORIZATION rls_regress_user0;
+ ALTER TABLE document SET ROW SECURITY FOR ALL TO (dauthor = current_user);
+ -- viewpoint from rls_regress_user1 again
+ SET SESSION AUTHORIZATION rls_regress_user1;
+ SELECT * FROM document WHERE f_leak(dtitle);
+ NOTICE: f_leak => my first novel
+ NOTICE: f_leak => my second novel
+ NOTICE: f_leak => my science fiction
+ NOTICE: f_leak => my first manga
+ NOTICE: f_leak => my second manga
+ did | cid | dlevel | dauthor | dtitle
+ -----+-----+--------+-------------------+--------------------
+ 1 | 11 | 1 | rls_regress_user1 | my first novel
+ 2 | 11 | 2 | rls_regress_user1 | my second novel
+ 3 | 22 | 2 | rls_regress_user1 | my science fiction
+ 4 | 44 | 1 | rls_regress_user1 | my first manga
+ 5 | 44 | 2 | rls_regress_user1 | my second manga
+ (5 rows)
+
+ SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+ NOTICE: f_leak => my first novel
+ NOTICE: f_leak => my second novel
+ NOTICE: f_leak => my science fiction
+ NOTICE: f_leak => my first manga
+ NOTICE: f_leak => my second manga
+ cid | did | dlevel | dauthor | dtitle | cname
+ -----+-----+--------+-------------------+--------------------+-----------------
+ 11 | 1 | 1 | rls_regress_user1 | my first novel | novel
+ 11 | 2 | 2 | rls_regress_user1 | my second novel | novel
+ 22 | 3 | 2 | rls_regress_user1 | my science fiction | science fiction
+ 44 | 4 | 1 | rls_regress_user1 | my first manga | manga
+ 44 | 5 | 2 | rls_regress_user1 | my second manga | manga
+ (5 rows)
+
+ -- viewpoint from rls_regress_user2 again
+ SET SESSION AUTHORIZATION rls_regress_user2;
+ SELECT * FROM document WHERE f_leak(dtitle);
+ NOTICE: f_leak => great science fiction
+ NOTICE: f_leak => great technology book
+ NOTICE: f_leak => great manga
+ did | cid | dlevel | dauthor | dtitle
+ -----+-----+--------+-------------------+-----------------------
+ 6 | 22 | 1 | rls_regress_user2 | great science fiction
+ 7 | 33 | 2 | rls_regress_user2 | great technology book
+ 8 | 44 | 1 | rls_regress_user2 | great manga
+ (3 rows)
+
+ SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+ NOTICE: f_leak => great science fiction
+ NOTICE: f_leak => great technology book
+ NOTICE: f_leak => great manga
+ cid | did | dlevel | dauthor | dtitle | cname
+ -----+-----+--------+-------------------+-----------------------+-----------------
+ 22 | 6 | 1 | rls_regress_user2 | great science fiction | science fiction
+ 33 | 7 | 2 | rls_regress_user2 | great technology book | technology
+ 44 | 8 | 1 | rls_regress_user2 | great manga | manga
+ (3 rows)
+
+ EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+ QUERY PLAN
+ ----------------------------------------------
+ Subquery Scan on document
+ Filter: f_leak(document.dtitle)
+ -> Seq Scan on document document_1
+ Filter: (dauthor = "current_user"())
+ (4 rows)
+
+ EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+ QUERY PLAN
+ ----------------------------------------------------
+ Nested Loop
+ -> Subquery Scan on document
+ Filter: f_leak(document.dtitle)
+ -> Seq Scan on document document_1
+ Filter: (dauthor = "current_user"())
+ -> Index Scan using category_pkey on category
+ Index Cond: (cid = document.cid)
+ (7 rows)
+
+ -- interaction of FK/PK constraints
+ SET SESSION AUTHORIZATION rls_regress_user0;
+ ALTER TABLE category SET ROW SECURITY FOR ALL
+ TO (CASE WHEN current_user = 'rls_regress_user1' THEN cid IN (11, 33)
+ WHEN current_user = 'rls_regress_user2' THEN cid IN (22, 44)
+ ELSE false END);
+ -- cannot delete PK referenced by invisible FK
+ SET SESSION AUTHORIZATION rls_regress_user1;
+ SELECT * FROM document d full outer join category c on d.cid = c.cid;
+ did | cid | dlevel | dauthor | dtitle | cid | cname
+ -----+-----+--------+-------------------+--------------------+-----+------------
+ 2 | 11 | 2 | rls_regress_user1 | my second novel | 11 | novel
+ 1 | 11 | 1 | rls_regress_user1 | my first novel | 11 | novel
+ | | | | | 33 | technology
+ 5 | 44 | 2 | rls_regress_user1 | my second manga | |
+ 4 | 44 | 1 | rls_regress_user1 | my first manga | |
+ 3 | 22 | 2 | rls_regress_user1 | my science fiction | |
+ (6 rows)
+
+ DELETE FROM category WHERE cid = 33; -- failed
+ ERROR: update or delete on table "category" violates foreign key constraint "document_cid_fkey" on table "document"
+ DETAIL: Key (cid)=(33) is still referenced from table "document".
+ -- cannot insert FK referencing invisible PK
+ SET SESSION AUTHORIZATION rls_regress_user2;
+ SELECT * FROM document d full outer join category c on d.cid = c.cid;
+ did | cid | dlevel | dauthor | dtitle | cid | cname
+ -----+-----+--------+-------------------+-----------------------+-----+-----------------
+ 6 | 22 | 1 | rls_regress_user2 | great science fiction | 22 | science fiction
+ 8 | 44 | 1 | rls_regress_user2 | great manga | 44 | manga
+ 7 | 33 | 2 | rls_regress_user2 | great technology book | |
+ (3 rows)
+
+ INSERT INTO document VALUES (10, 33, 1, current_user, 'hoge'); -- failed
+ ERROR: insert or update on table "document" violates foreign key constraint "document_cid_fkey"
+ DETAIL: Key (cid)=(33) is not present in table "category".
+ -- database superuser can bypass RLS policy
+ RESET SESSION AUTHORIZATION;
+ SELECT * FROM document;
+ did | cid | dlevel | dauthor | dtitle
+ -----+-----+--------+-------------------+-----------------------
+ 1 | 11 | 1 | rls_regress_user1 | my first novel
+ 2 | 11 | 2 | rls_regress_user1 | my second novel
+ 3 | 22 | 2 | rls_regress_user1 | my science fiction
+ 4 | 44 | 1 | rls_regress_user1 | my first manga
+ 5 | 44 | 2 | rls_regress_user1 | my second manga
+ 6 | 22 | 1 | rls_regress_user2 | great science fiction
+ 7 | 33 | 2 | rls_regress_user2 | great technology book
+ 8 | 44 | 1 | rls_regress_user2 | great manga
+ (8 rows)
+
+ SELECT * FROM category;
+ cid | cname
+ -----+-----------------
+ 11 | novel
+ 22 | science fiction
+ 33 | technology
+ 44 | manga
+ (4 rows)
+
+ --
+ -- Table inheritance and RLS policy
+ --
+ SET SESSION AUTHORIZATION rls_regress_user0;
+ CREATE TABLE t1 (a int, junk1 text, b text) WITH OIDS;
+ ALTER TABLE t1 DROP COLUMN junk1; -- just a disturbing factor
+ GRANT ALL ON t1 TO public;
+ COPY t1 FROM stdin WITH (oids);
+ CREATE TABLE t2 (c float) INHERITS (t1);
+ COPY t2 FROM stdin WITH (oids);
+ CREATE TABLE t3 (c text, b text, a int) WITH OIDS;
+ ALTER TABLE t3 INHERIT t1;
+ COPY t3(a,b,c) FROM stdin WITH (oids);
+ ALTER TABLE t1 SET ROW SECURITY FOR ALL TO (a % 2 = 0); -- be even number
+ ALTER TABLE t2 SET ROW SECURITY FOR ALL TO (a % 2 = 1); -- be odd number
+ SELECT * FROM t1;
+ a | b
+ ---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+ (7 rows)
+
+ EXPLAIN (costs off) SELECT * FROM t1;
+ QUERY PLAN
+ -------------------------------------
+ Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+ (8 rows)
+
+ SELECT * FROM t1 WHERE f_leak(b);
+ NOTICE: f_leak => bbb
+ NOTICE: f_leak => ddd
+ NOTICE: f_leak => abc
+ NOTICE: f_leak => cde
+ NOTICE: f_leak => xxx
+ NOTICE: f_leak => yyy
+ NOTICE: f_leak => zzz
+ a | b
+ ---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+ (7 rows)
+
+ EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+ QUERY PLAN
+ -------------------------------------
+ Append
+ -> Subquery Scan on t1
+ Filter: f_leak(t1.b)
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ Filter: f_leak(t2.b)
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+ Filter: f_leak(b)
+ (11 rows)
+
+ -- reference to system column
+ SELECT oid, * FROM t1;
+ oid | a | b
+ -----+---+-----
+ 102 | 2 | bbb
+ 104 | 4 | ddd
+ 201 | 1 | abc
+ 203 | 3 | cde
+ 301 | 1 | xxx
+ 302 | 2 | yyy
+ 303 | 3 | zzz
+ (7 rows)
+
+ EXPLAIN (costs off) SELECT * FROM t1;
+ QUERY PLAN
+ -------------------------------------
+ Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+ (8 rows)
+
+ -- reference to whole-row reference
+ SELECT *,t1 FROM t1;
+ a | b | t1
+ ---+-----+---------
+ 2 | bbb | (2,bbb)
+ 4 | ddd | (4,ddd)
+ 1 | abc | (1,abc)
+ 3 | cde | (3,cde)
+ 1 | xxx | (1,xxx)
+ 2 | yyy | (2,yyy)
+ 3 | zzz | (3,zzz)
+ (7 rows)
+
+ EXPLAIN (costs off) SELECT *,t1 FROM t1;
+ QUERY PLAN
+ -------------------------------------
+ Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+ (8 rows)
+
+ -- for share/update lock
+ SELECT * FROM t1 FOR SHARE;
+ a | b
+ ---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+ (7 rows)
+
+ EXPLAIN (costs off) SELECT * FROM t1 FOR SHARE;
+ QUERY PLAN
+ -------------------------------------------
+ LockRows
+ -> Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+ (9 rows)
+
+ SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+ NOTICE: f_leak => bbb
+ NOTICE: f_leak => ddd
+ NOTICE: f_leak => abc
+ NOTICE: f_leak => cde
+ NOTICE: f_leak => xxx
+ NOTICE: f_leak => yyy
+ NOTICE: f_leak => zzz
+ a | b
+ ---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+ (7 rows)
+
+ EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+ QUERY PLAN
+ -------------------------------------------
+ LockRows
+ -> Append
+ -> Subquery Scan on t1
+ Filter: f_leak(t1.b)
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ Filter: f_leak(t2.b)
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+ Filter: f_leak(b)
+ (12 rows)
+
+ --
+ -- COPY TO statement
+ --
+ COPY t1 TO stdout;
+ 2 bbb
+ 4 ddd
+ COPY t1 TO stdout WITH OIDS;
+ 102 2 bbb
+ 104 4 ddd
+ COPY t2(c,b) TO stdout WITH OIDS;
+ 201 1.1 abc
+ 203 3.3 cde
+ COPY (SELECT * FROM t1) TO stdout;
+ 2 bbb
+ 4 ddd
+ 1 abc
+ 3 cde
+ 1 xxx
+ 2 yyy
+ 3 zzz
+ COPY document TO stdout WITH OIDS; -- failed (no oid column)
+ ERROR: table "document" does not have OIDs
+ --
+ -- recursive RLS and VIEWs in policy
+ --
+ CREATE TABLE s1 (a int, b text);
+ INSERT INTO s1 (SELECT x, md5(x::text) FROM generate_series(-10,10) x);
+ CREATE TABLE s2 (x int, y text);
+ INSERT INTO s2 (SELECT x, md5(x::text) FROM generate_series(-6,6) x);
+ CREATE VIEW v2 AS SELECT * FROM s2 WHERE y like '%af%';
+ ALTER TABLE s1 SET ROW SECURITY FOR ALL
+ TO (a in (select x from s2 where y like '%2f%'));
+ ALTER TABLE s2 SET ROW SECURITY FOR ALL
+ TO (x in (select a from s1 where b like '%22%'));
+ SELECT * FROM s1 WHERE f_leak(b); -- fail (infinite recursion)
+ ERROR: infinite recursion detected for relation "s1"
+ ALTER TABLE s2 SET ROW SECURITY FOR ALL TO (x % 2 = 0);
+ SELECT * FROM s1 WHERE f_leak(b); -- OK
+ NOTICE: f_leak => c81e728d9d4c2f636f067f89cc14862c
+ NOTICE: f_leak => a87ff679a2f3e71d9181a67b7542122c
+ a | b
+ ---+----------------------------------
+ 2 | c81e728d9d4c2f636f067f89cc14862c
+ 4 | a87ff679a2f3e71d9181a67b7542122c
+ (2 rows)
+
+ EXPLAIN SELECT * FROM only s1 WHERE f_leak(b);
+ QUERY PLAN
+ ---------------------------------------------------------------------------------------
+ Subquery Scan on s1 (cost=28.55..61.67 rows=205 width=36)
+ Filter: f_leak(s1.b)
+ -> Hash Join (cost=28.55..55.52 rows=615 width=36)
+ Hash Cond: (s1_1.a = s2.x)
+ -> Seq Scan on s1 s1_1 (cost=0.00..22.30 rows=1230 width=36)
+ -> Hash (cost=28.54..28.54 rows=1 width=4)
+ -> HashAggregate (cost=28.53..28.54 rows=1 width=4)
+ -> Subquery Scan on s2 (cost=0.00..28.52 rows=1 width=4)
+ Filter: (s2.y ~~ '%2f%'::text)
+ -> Seq Scan on s2 s2_1 (cost=0.00..28.45 rows=6 width=36)
+ Filter: ((x % 2) = 0)
+ (11 rows)
+
+ ALTER TABLE s1 SET ROW SECURITY FOR ALL
+ TO (a in (select x from v2)); -- using VIEW in RLS policy
+ SELECT * FROM s1 WHERE f_leak(b); -- OK
+ NOTICE: f_leak => 0267aaf632e87a63288a08331f22c7c3
+ NOTICE: f_leak => 1679091c5a880faf6fb5e6087eb1b2dc
+ a | b
+ ----+----------------------------------
+ -4 | 0267aaf632e87a63288a08331f22c7c3
+ 6 | 1679091c5a880faf6fb5e6087eb1b2dc
+ (2 rows)
+
+ EXPLAIN (COSTS OFF) SELECT * FROM s1 WHERE f_leak(b);
+ QUERY PLAN
+ ----------------------------------------------------------
+ Subquery Scan on s1
+ Filter: f_leak(s1.b)
+ -> Hash Join
+ Hash Cond: (s1_1.a = s2.x)
+ -> Seq Scan on s1 s1_1
+ -> Hash
+ -> HashAggregate
+ -> Subquery Scan on s2
+ Filter: (s2.y ~~ '%af%'::text)
+ -> Seq Scan on s2 s2_1
+ Filter: ((x % 2) = 0)
+ (11 rows)
+
+ SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+ xx | x | y
+ ----+----+----------------------------------
+ -6 | -6 | 596a3d04481816330f07e4f97510c28f
+ -4 | -4 | 0267aaf632e87a63288a08331f22c7c3
+ 2 | 2 | c81e728d9d4c2f636f067f89cc14862c
+ (3 rows)
+
+ EXPLAIN (COSTS OFF) SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+ QUERY PLAN
+ --------------------------------------------------------------------
+ Subquery Scan on s2
+ Filter: (s2.y ~~ '%28%'::text)
+ -> Seq Scan on s2 s2_1
+ Filter: ((x % 2) = 0)
+ SubPlan 1
+ -> Limit
+ -> Subquery Scan on s1
+ -> Nested Loop Semi Join
+ Join Filter: (s1_1.a = s2_2.x)
+ -> Seq Scan on s1 s1_1
+ -> Materialize
+ -> Subquery Scan on s2_2
+ Filter: (s2_2.y ~~ '%af%'::text)
+ -> Seq Scan on s2 s2_3
+ Filter: ((x % 2) = 0)
+ (15 rows)
+
+ ALTER TABLE s2 SET ROW SECURITY FOR ALL
+ TO (x in (select a from s1 where b like '%d2%'));
+ SELECT * FROM s1 WHERE f_leak(b); -- fail (infinite recursion via view)
+ ERROR: infinite recursion detected for relation "s1"
+ -- prepared statement with rls_regress_user0 privilege
+ PREPARE p1(int) AS SELECT * FROM t1 WHERE a <= $1;
+ EXECUTE p1(2);
+ a | b
+ ---+-----
+ 2 | bbb
+ 1 | abc
+ 1 | xxx
+ 2 | yyy
+ (4 rows)
+
+ EXPLAIN (costs off) EXECUTE p1(2);
+ QUERY PLAN
+ ----------------------------------------------------
+ Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a <= 2) AND ((a % 2) = 0))
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a <= 2) AND ((a % 2) = 1))
+ -> Seq Scan on t3
+ Filter: (a <= 2)
+ (9 rows)
+
+ -- superuser is allowed to bypass RLS checks
+ RESET SESSION AUTHORIZATION;
+ SELECT * FROM t1 WHERE f_leak(b);
+ NOTICE: f_leak => aaa
+ NOTICE: f_leak => bbb
+ NOTICE: f_leak => ccc
+ NOTICE: f_leak => ddd
+ NOTICE: f_leak => abc
+ NOTICE: f_leak => bcd
+ NOTICE: f_leak => cde
+ NOTICE: f_leak => def
+ NOTICE: f_leak => xxx
+ NOTICE: f_leak => yyy
+ NOTICE: f_leak => zzz
+ a | b
+ ---+-----
+ 1 | aaa
+ 2 | bbb
+ 3 | ccc
+ 4 | ddd
+ 1 | abc
+ 2 | bcd
+ 3 | cde
+ 4 | def
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+ (11 rows)
+
+ EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+ QUERY PLAN
+ ---------------------------
+ Append
+ -> Seq Scan on t1
+ Filter: f_leak(b)
+ -> Seq Scan on t2
+ Filter: f_leak(b)
+ -> Seq Scan on t3
+ Filter: f_leak(b)
+ (7 rows)
+
+ -- plan cache should be invalidated
+ EXECUTE p1(2);
+ a | b
+ ---+-----
+ 1 | aaa
+ 2 | bbb
+ 1 | abc
+ 2 | bcd
+ 1 | xxx
+ 2 | yyy
+ (6 rows)
+
+ EXPLAIN (costs off) EXECUTE p1(2);
+ QUERY PLAN
+ --------------------------
+ Append
+ -> Seq Scan on t1
+ Filter: (a <= 2)
+ -> Seq Scan on t2
+ Filter: (a <= 2)
+ -> Seq Scan on t3
+ Filter: (a <= 2)
+ (7 rows)
+
+ PREPARE p2(int) AS SELECT * FROM t1 WHERE a = $1;
+ EXECUTE p2(2);
+ a | b
+ ---+-----
+ 2 | bbb
+ 2 | bcd
+ 2 | yyy
+ (3 rows)
+
+ EXPLAIN (costs off) EXECUTE p2(2);
+ QUERY PLAN
+ -------------------------
+ Append
+ -> Seq Scan on t1
+ Filter: (a = 2)
+ -> Seq Scan on t2
+ Filter: (a = 2)
+ -> Seq Scan on t3
+ Filter: (a = 2)
+ (7 rows)
+
+ -- also, case when privilege switch from superuser
+ SET SESSION AUTHORIZATION rls_regress_user0;
+ EXECUTE p2(2);
+ a | b
+ ---+-----
+ 2 | bbb
+ 2 | yyy
+ (2 rows)
+
+ EXPLAIN (costs off) EXECUTE p2(2);
+ QUERY PLAN
+ ---------------------------------------------------
+ Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a = 2) AND ((a % 2) = 0))
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a = 2) AND ((a % 2) = 1))
+ -> Seq Scan on t3
+ Filter: (a = 2)
+ (9 rows)
+
+ --
+ -- UPDATE / DELETE and Row-level security
+ --
+ SET SESSION AUTHORIZATION rls_regress_user0;
+ EXPLAIN (costs off) UPDATE t1 SET b = b || b WHERE f_leak(b);
+ QUERY PLAN
+ -------------------------------------
+ Update on t1
+ -> Subquery Scan on t1_1
+ Filter: f_leak(t1_1.b)
+ -> Seq Scan on t1 t1_2
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ Filter: f_leak(t2.b)
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+ Filter: f_leak(b)
+ (11 rows)
+
+ UPDATE t1 SET b = b || b WHERE f_leak(b);
+ NOTICE: f_leak => bbb
+ NOTICE: f_leak => ddd
+ NOTICE: f_leak => abc
+ NOTICE: f_leak => cde
+ NOTICE: f_leak => xxx
+ NOTICE: f_leak => yyy
+ NOTICE: f_leak => zzz
+ EXPLAIN (costs off) UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b);
+ QUERY PLAN
+ -------------------------------------
+ Update on t1
+ -> Subquery Scan on t1_1
+ Filter: f_leak(t1_1.b)
+ -> Seq Scan on t1 t1_2
+ Filter: ((a % 2) = 0)
+ (5 rows)
+
+ UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b);
+ NOTICE: f_leak => bbbbbb
+ NOTICE: f_leak => dddddd
+ -- returning clause with system column
+ UPDATE only t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+ NOTICE: f_leak => bbbbbb_updt
+ NOTICE: f_leak => dddddd_updt
+ oid | a | b | t1
+ -----+---+-------------+-----------------
+ 102 | 2 | bbbbbb_updt | (2,bbbbbb_updt)
+ 104 | 4 | dddddd_updt | (4,dddddd_updt)
+ (2 rows)
+
+ UPDATE t1 SET b = b WHERE f_leak(b) RETURNING *;
+ NOTICE: f_leak => bbbbbb_updt
+ NOTICE: f_leak => dddddd_updt
+ NOTICE: f_leak => abcabc
+ NOTICE: f_leak => cdecde
+ NOTICE: f_leak => xxxxxx
+ NOTICE: f_leak => yyyyyy
+ NOTICE: f_leak => zzzzzz
+ a | b
+ ---+-------------
+ 2 | bbbbbb_updt
+ 4 | dddddd_updt
+ 1 | abcabc
+ 3 | cdecde
+ 1 | xxxxxx
+ 2 | yyyyyy
+ 3 | zzzzzz
+ (7 rows)
+
+ UPDATE t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+ NOTICE: f_leak => bbbbbb_updt
+ NOTICE: f_leak => dddddd_updt
+ NOTICE: f_leak => abcabc
+ NOTICE: f_leak => cdecde
+ NOTICE: f_leak => xxxxxx
+ NOTICE: f_leak => yyyyyy
+ NOTICE: f_leak => zzzzzz
+ oid | a | b | t1
+ -----+---+-------------+-----------------
+ 102 | 2 | bbbbbb_updt | (2,bbbbbb_updt)
+ 104 | 4 | dddddd_updt | (4,dddddd_updt)
+ 201 | 1 | abcabc | (1,abcabc)
+ 203 | 3 | cdecde | (3,cdecde)
+ 301 | 1 | xxxxxx | (1,xxxxxx)
+ 302 | 2 | yyyyyy | (2,yyyyyy)
+ 303 | 3 | zzzzzz | (3,zzzzzz)
+ (7 rows)
+
+ RESET SESSION AUTHORIZATION;
+ SELECT * FROM t1;
+ a | b
+ ---+-------------
+ 1 | aaa
+ 3 | ccc
+ 2 | bbbbbb_updt
+ 4 | dddddd_updt
+ 2 | bcd
+ 4 | def
+ 1 | abcabc
+ 3 | cdecde
+ 1 | xxxxxx
+ 2 | yyyyyy
+ 3 | zzzzzz
+ (11 rows)
+
+ SET SESSION AUTHORIZATION rls_regress_user0;
+ EXPLAIN (costs off) DELETE FROM only t1 WHERE f_leak(b);
+ QUERY PLAN
+ -------------------------------------
+ Delete on t1
+ -> Subquery Scan on t1_1
+ Filter: f_leak(t1_1.b)
+ -> Seq Scan on t1 t1_2
+ Filter: ((a % 2) = 0)
+ (5 rows)
+
+ EXPLAIN (costs off) DELETE FROM t1 WHERE f_leak(b);
+ QUERY PLAN
+ -------------------------------------
+ Delete on t1
+ -> Subquery Scan on t1_1
+ Filter: f_leak(t1_1.b)
+ -> Seq Scan on t1 t1_2
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ Filter: f_leak(t2.b)
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+ Filter: f_leak(b)
+ (11 rows)
+
+ DELETE FROM only t1 WHERE f_leak(b) RETURNING oid, *, t1;
+ NOTICE: f_leak => bbbbbb_updt
+ NOTICE: f_leak => dddddd_updt
+ oid | a | b | t1
+ -----+---+-------------+-----------------
+ 102 | 2 | bbbbbb_updt | (2,bbbbbb_updt)
+ 104 | 4 | dddddd_updt | (4,dddddd_updt)
+ (2 rows)
+
+ DELETE FROM t1 WHERE f_leak(b) RETURNING oid, *, t1;
+ NOTICE: f_leak => abcabc
+ NOTICE: f_leak => cdecde
+ NOTICE: f_leak => xxxxxx
+ NOTICE: f_leak => yyyyyy
+ NOTICE: f_leak => zzzzzz
+ oid | a | b | t1
+ -----+---+--------+------------
+ 201 | 1 | abcabc | (1,abcabc)
+ 203 | 3 | cdecde | (3,cdecde)
+ 301 | 1 | xxxxxx | (1,xxxxxx)
+ 302 | 2 | yyyyyy | (2,yyyyyy)
+ 303 | 3 | zzzzzz | (3,zzzzzz)
+ (5 rows)
+
+ --
+ -- Test psql \dt+ command
+ --
+ ALTER TABLE category RESET ROW SECURITY FOR ALL; -- too long qual
+ \dt+
+ List of relations
+ Schema | Name | Type | Owner | Size | Description | Row-security
+ --------------------+----------+-------+-------------------+------------+-------------+----------------------------------
+ rls_regress_schema | category | table | rls_regress_user0 | 16 kB | |
+ rls_regress_schema | document | table | rls_regress_user0 | 16 kB | | (dauthor = "current_user"())
+ rls_regress_schema | s1 | table | rls_regress_user0 | 16 kB | | (a IN ( SELECT v2.x +
+ | | | | | | FROM v2))
+ rls_regress_schema | s2 | table | rls_regress_user0 | 16 kB | | (x IN ( SELECT s1.a +
+ | | | | | | FROM s1 +
+ | | | | | | WHERE (s1.b ~~ '%d2%'::text)))
+ rls_regress_schema | t1 | table | rls_regress_user0 | 16 kB | | ((a % 2) = 0)
+ rls_regress_schema | t2 | table | rls_regress_user0 | 16 kB | | ((a % 2) = 1)
+ rls_regress_schema | t3 | table | rls_regress_user0 | 16 kB | |
+ rls_regress_schema | uaccount | table | rls_regress_user0 | 8192 bytes | |
+ (8 rows)
+
+ --
+ -- Clean up objects
+ --
+ RESET SESSION AUTHORIZATION;
+ DROP SCHEMA rls_regress_schema CASCADE;
+ NOTICE: drop cascades to 10 other objects
+ DETAIL: drop cascades to function f_leak(text)
+ drop cascades to table uaccount
+ drop cascades to table category
+ drop cascades to table document
+ drop cascades to table t1
+ drop cascades to table t2
+ drop cascades to table t3
+ drop cascades to table s1
+ drop cascades to table s2
+ drop cascades to view v2
+ DROP USER rls_regress_user0;
+ DROP USER rls_regress_user1;
+ DROP USER rls_regress_user2;
diff --git a/src/test/regress/expected/sanity_check.out b/src/test/regress/expected/sanity_check.out
new file mode 100644
index a62a3e3..a41344b
*** a/src/test/regress/expected/sanity_check.out
--- b/src/test/regress/expected/sanity_check.out
*************** pg_pltemplate|t
*** 121,126 ****
--- 121,127 ----
pg_proc|t
pg_range|t
pg_rewrite|t
+ pg_rowsecurity|t
pg_seclabel|t
pg_shdepend|t
pg_shdescription|t
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
new file mode 100644
index 5758b07..f7963fc
*** a/src/test/regress/parallel_schedule
--- b/src/test/regress/parallel_schedule
*************** test: select_into select_distinct select
*** 83,89 ****
# ----------
# Another group of parallel tests
# ----------
! test: privileges security_label collate matview lock replica_identity
# ----------
# Another group of parallel tests
--- 83,89 ----
# ----------
# Another group of parallel tests
# ----------
! test: privileges rowsecurity security_label collate matview lock replica_identity
# ----------
# Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
new file mode 100644
index 78348f5..51c4c12
*** a/src/test/regress/serial_schedule
--- b/src/test/regress/serial_schedule
*************** test: delete
*** 94,99 ****
--- 94,100 ----
test: namespace
test: prepared_xacts
test: privileges
+ test: rowsecurity
test: security_label
test: collate
test: matview
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
new file mode 100644
index ...55d4aad
*** a/src/test/regress/sql/rowsecurity.sql
--- b/src/test/regress/sql/rowsecurity.sql
***************
*** 0 ****
--- 1,298 ----
+ --
+ -- Test of Row-level security feature
+ --
+
+ -- Clean up in case a prior regression run failed
+
+ -- Suppress NOTICE messages when users/groups don't exist
+ SET client_min_messages TO 'warning';
+
+ DROP USER IF EXISTS rls_regress_user0;
+ DROP USER IF EXISTS rls_regress_user1;
+ DROP USER IF EXISTS rls_regress_user2;
+
+ DROP SCHEMA IF EXISTS rls_regress_schema CASCADE;
+
+ RESET client_min_messages;
+
+ -- initial setup
+ CREATE USER rls_regress_user0;
+ CREATE USER rls_regress_user1;
+ CREATE USER rls_regress_user2;
+
+ CREATE SCHEMA rls_regress_schema;
+ GRANT ALL ON SCHEMA rls_regress_schema TO public;
+ SET search_path = rls_regress_schema;
+
+ -- setup of malicious function
+ CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool
+ COST 0.0000001 LANGUAGE plpgsql
+ AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END';
+ GRANT EXECUTE ON FUNCTION f_leak(text) TO public;
+
+ -- BASIC Row-Level Security Scenario
+
+ SET SESSION AUTHORIZATION rls_regress_user0;
+ CREATE TABLE uaccount (
+ pguser name primary key,
+ seclv int
+ );
+ INSERT INTO uaccount VALUES
+ ('rls_regress_user0', 99),
+ ('rls_regress_user1', 1),
+ ('rls_regress_user2', 2),
+ ('rls_regress_user3', 3);
+ GRANT SELECT ON uaccount TO public;
+
+ CREATE TABLE category (
+ cid int primary key,
+ cname text
+ );
+ GRANT ALL ON category TO public;
+ INSERT INTO category VALUES
+ (11, 'novel'),
+ (22, 'science fiction'),
+ (33, 'technology'),
+ (44, 'manga');
+
+ CREATE TABLE document (
+ did int primary key,
+ cid int references category(cid),
+ dlevel int not null,
+ dauthor name,
+ dtitle text
+ );
+ GRANT ALL ON document TO public;
+ INSERT INTO document VALUES
+ ( 1, 11, 1, 'rls_regress_user1', 'my first novel'),
+ ( 2, 11, 2, 'rls_regress_user1', 'my second novel'),
+ ( 3, 22, 2, 'rls_regress_user1', 'my science fiction'),
+ ( 4, 44, 1, 'rls_regress_user1', 'my first manga'),
+ ( 5, 44, 2, 'rls_regress_user1', 'my second manga'),
+ ( 6, 22, 1, 'rls_regress_user2', 'great science fiction'),
+ ( 7, 33, 2, 'rls_regress_user2', 'great technology book'),
+ ( 8, 44, 1, 'rls_regress_user2', 'great manga');
+
+ -- user's security level must higher than or equal to document's one
+ ALTER TABLE document SET ROW SECURITY FOR ALL
+ TO (dlevel <= (SELECT seclv FROM uaccount WHERE pguser = current_user));
+
+ -- viewpoint from rls_regress_user1
+ SET SESSION AUTHORIZATION rls_regress_user1;
+ SELECT * FROM document WHERE f_leak(dtitle);
+ SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+ -- viewpoint from rls_regress_user2
+ SET SESSION AUTHORIZATION rls_regress_user2;
+ SELECT * FROM document WHERE f_leak(dtitle);
+ SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+ EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+ EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+ -- only owner can change row-level security
+ ALTER TABLE document SET ROW SECURITY FOR ALL TO (true); -- fail
+ ALTER TABLE document RESET ROW SECURITY FOR ALL; -- fail
+
+ SET SESSION AUTHORIZATION rls_regress_user0;
+ ALTER TABLE document SET ROW SECURITY FOR ALL TO (dauthor = current_user);
+
+ -- viewpoint from rls_regress_user1 again
+ SET SESSION AUTHORIZATION rls_regress_user1;
+ SELECT * FROM document WHERE f_leak(dtitle);
+ SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+ -- viewpoint from rls_regress_user2 again
+ SET SESSION AUTHORIZATION rls_regress_user2;
+ SELECT * FROM document WHERE f_leak(dtitle);
+ SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+ EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+ EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+ -- interaction of FK/PK constraints
+ SET SESSION AUTHORIZATION rls_regress_user0;
+ ALTER TABLE category SET ROW SECURITY FOR ALL
+ TO (CASE WHEN current_user = 'rls_regress_user1' THEN cid IN (11, 33)
+ WHEN current_user = 'rls_regress_user2' THEN cid IN (22, 44)
+ ELSE false END);
+
+ -- cannot delete PK referenced by invisible FK
+ SET SESSION AUTHORIZATION rls_regress_user1;
+ SELECT * FROM document d full outer join category c on d.cid = c.cid;
+ DELETE FROM category WHERE cid = 33; -- failed
+
+ -- cannot insert FK referencing invisible PK
+ SET SESSION AUTHORIZATION rls_regress_user2;
+ SELECT * FROM document d full outer join category c on d.cid = c.cid;
+ INSERT INTO document VALUES (10, 33, 1, current_user, 'hoge'); -- failed
+
+ -- database superuser can bypass RLS policy
+ RESET SESSION AUTHORIZATION;
+ SELECT * FROM document;
+ SELECT * FROM category;
+
+ --
+ -- Table inheritance and RLS policy
+ --
+ SET SESSION AUTHORIZATION rls_regress_user0;
+
+ CREATE TABLE t1 (a int, junk1 text, b text) WITH OIDS;
+ ALTER TABLE t1 DROP COLUMN junk1; -- just a disturbing factor
+ GRANT ALL ON t1 TO public;
+
+ COPY t1 FROM stdin WITH (oids);
+ 101 1 aaa
+ 102 2 bbb
+ 103 3 ccc
+ 104 4 ddd
+ \.
+
+ CREATE TABLE t2 (c float) INHERITS (t1);
+ COPY t2 FROM stdin WITH (oids);
+ 201 1 abc 1.1
+ 202 2 bcd 2.2
+ 203 3 cde 3.3
+ 204 4 def 4.4
+ \.
+
+ CREATE TABLE t3 (c text, b text, a int) WITH OIDS;
+ ALTER TABLE t3 INHERIT t1;
+ COPY t3(a,b,c) FROM stdin WITH (oids);
+ 301 1 xxx X
+ 302 2 yyy Y
+ 303 3 zzz Z
+ \.
+
+ ALTER TABLE t1 SET ROW SECURITY FOR ALL TO (a % 2 = 0); -- be even number
+ ALTER TABLE t2 SET ROW SECURITY FOR ALL TO (a % 2 = 1); -- be odd number
+
+ SELECT * FROM t1;
+ EXPLAIN (costs off) SELECT * FROM t1;
+
+ SELECT * FROM t1 WHERE f_leak(b);
+ EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+
+ -- reference to system column
+ SELECT oid, * FROM t1;
+ EXPLAIN (costs off) SELECT * FROM t1;
+
+ -- reference to whole-row reference
+ SELECT *,t1 FROM t1;
+ EXPLAIN (costs off) SELECT *,t1 FROM t1;
+
+ -- for share/update lock
+ SELECT * FROM t1 FOR SHARE;
+ EXPLAIN (costs off) SELECT * FROM t1 FOR SHARE;
+
+ SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+ EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+
+ --
+ -- COPY TO statement
+ --
+ COPY t1 TO stdout;
+ COPY t1 TO stdout WITH OIDS;
+ COPY t2(c,b) TO stdout WITH OIDS;
+ COPY (SELECT * FROM t1) TO stdout;
+ COPY document TO stdout WITH OIDS; -- failed (no oid column)
+
+ --
+ -- recursive RLS and VIEWs in policy
+ --
+ CREATE TABLE s1 (a int, b text);
+ INSERT INTO s1 (SELECT x, md5(x::text) FROM generate_series(-10,10) x);
+
+ CREATE TABLE s2 (x int, y text);
+ INSERT INTO s2 (SELECT x, md5(x::text) FROM generate_series(-6,6) x);
+ CREATE VIEW v2 AS SELECT * FROM s2 WHERE y like '%af%';
+
+ ALTER TABLE s1 SET ROW SECURITY FOR ALL
+ TO (a in (select x from s2 where y like '%2f%'));
+
+ ALTER TABLE s2 SET ROW SECURITY FOR ALL
+ TO (x in (select a from s1 where b like '%22%'));
+
+ SELECT * FROM s1 WHERE f_leak(b); -- fail (infinite recursion)
+
+ ALTER TABLE s2 SET ROW SECURITY FOR ALL TO (x % 2 = 0);
+
+ SELECT * FROM s1 WHERE f_leak(b); -- OK
+ EXPLAIN SELECT * FROM only s1 WHERE f_leak(b);
+
+ ALTER TABLE s1 SET ROW SECURITY FOR ALL
+ TO (a in (select x from v2)); -- using VIEW in RLS policy
+ SELECT * FROM s1 WHERE f_leak(b); -- OK
+ EXPLAIN (COSTS OFF) SELECT * FROM s1 WHERE f_leak(b);
+
+ SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+ EXPLAIN (COSTS OFF) SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+
+ ALTER TABLE s2 SET ROW SECURITY FOR ALL
+ TO (x in (select a from s1 where b like '%d2%'));
+ SELECT * FROM s1 WHERE f_leak(b); -- fail (infinite recursion via view)
+
+ -- prepared statement with rls_regress_user0 privilege
+ PREPARE p1(int) AS SELECT * FROM t1 WHERE a <= $1;
+ EXECUTE p1(2);
+ EXPLAIN (costs off) EXECUTE p1(2);
+
+ -- superuser is allowed to bypass RLS checks
+ RESET SESSION AUTHORIZATION;
+ SELECT * FROM t1 WHERE f_leak(b);
+ EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+
+ -- plan cache should be invalidated
+ EXECUTE p1(2);
+ EXPLAIN (costs off) EXECUTE p1(2);
+
+ PREPARE p2(int) AS SELECT * FROM t1 WHERE a = $1;
+ EXECUTE p2(2);
+ EXPLAIN (costs off) EXECUTE p2(2);
+
+ -- also, case when privilege switch from superuser
+ SET SESSION AUTHORIZATION rls_regress_user0;
+ EXECUTE p2(2);
+ EXPLAIN (costs off) EXECUTE p2(2);
+
+ --
+ -- UPDATE / DELETE and Row-level security
+ --
+ SET SESSION AUTHORIZATION rls_regress_user0;
+ EXPLAIN (costs off) UPDATE t1 SET b = b || b WHERE f_leak(b);
+ UPDATE t1 SET b = b || b WHERE f_leak(b);
+
+ EXPLAIN (costs off) UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b);
+ UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b);
+
+ -- returning clause with system column
+ UPDATE only t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+ UPDATE t1 SET b = b WHERE f_leak(b) RETURNING *;
+ UPDATE t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+
+ RESET SESSION AUTHORIZATION;
+ SELECT * FROM t1;
+
+ SET SESSION AUTHORIZATION rls_regress_user0;
+ EXPLAIN (costs off) DELETE FROM only t1 WHERE f_leak(b);
+ EXPLAIN (costs off) DELETE FROM t1 WHERE f_leak(b);
+
+ DELETE FROM only t1 WHERE f_leak(b) RETURNING oid, *, t1;
+ DELETE FROM t1 WHERE f_leak(b) RETURNING oid, *, t1;
+
+ --
+ -- Test psql \dt+ command
+ --
+ ALTER TABLE category RESET ROW SECURITY FOR ALL; -- too long qual
+ \dt+
+
+ --
+ -- Clean up objects
+ --
+ RESET SESSION AUTHORIZATION;
+
+ DROP SCHEMA rls_regress_schema CASCADE;
+
+ DROP USER rls_regress_user0;
+ DROP USER rls_regress_user1;
+ DROP USER rls_regress_user2;
On 12/14/2013 11:24 AM, Gregory Smith wrote:
The RLS feature set available with the CF submission is good enough
for those projects to continue exploring PostgreSQL
You may want to check out the updated writable security-barrier views patch.
/messages/by-id/52AB112B.6020403@2ndquadrant.com
It may offer a path forward for the CF submission for RLS, letting us
get rid of the var/attr fiddling that many here objected to.
There's some interesting bad
news, like how we're going to have to completely refactor all the "if
(!superuser())" shortcuts into real permissions one day to make them
happy.
We'll need that for mandatory access control. Initially I'd probably
want to do it through SEPostgreSQL role checks, but it'd be desirable to
have a generic mechanism that'd work with any OS's role management
eventually.
I think we're going to need finer grained rights than we have now, as
"superuser or not" isn't really good enough for some things. In
particular, the fact that if plperl or plpython are installed the
superuser (often accessible remotely) can trivially run arbitrary
C-level functions and invoke commands as the PostgreSQL OS user isn't
going to be acceptable in these environments, but they may still want
the trusted PLs.
Attached is a small demo piece built by my new co-worker Jeff McCormick
that I've been hacking lightly on. The idea was to get something a
step beyond trivial that maps into real-world need, well enough that
we could get people to agree it was usefully representative.
I really appreciate that. I suspect more work will be needed on the
interface for the actual row-level security code, and work like this
helps point in the direction of what's needed.
One reason the users I've been talking to feel
comfortable that they'll be able to build apps usefully with this
feature is this interface. It feels more like a pluggable API for
defining rules than a specific implementation. Congratulations to the
usual community members who expect complete feature overkill in every
design.
There's a C-level API that's completely pluggable, for what it's worth.
I'm actually unconvinced that the SQL-level API for row-security is
appropriate - I'm persuaded by comments made on the list that the RLS
syntax and security model needs to be easy enough to use to be useful if
it's to add much on top of what we get with updatable s.b. views,
security barrier, leakproof, etc.
I don't think "run some SQL for every row" fits that. We have that at
the C level in the RLS patch, and I wonder if it should stay there.
I find the heirachical and non-heirachical label security model used in
Teradata to be extremely interesting and worthy of study.
The concept is that there are heirachical label policies (think
"classified", "unclassified", etc) or non-heirachical (mutually
exclusive labels). A user can see a row if they have all the policies
used in a table, and each of the user's policy values is sufficient to
see the row. For non-heiracical policies that means the user and row
must have at least one label in common; for heiracical policies it means
the user label must be greater than or equal to the row label.
That model lets you quite flexibly and easily build complex security
models. It'd work well with PostgreSQL, too: We could implement
something like non-heirachical policies using a system varbit column
that's added whenever a non-heirachical policy gets added to a table,
and simply AND it with the varbit on the user's policy to see if they
can access the row. Or use a compact fixed-width bitfield. Heirachical
policies would use a 1 or 2 byte int system column to store the label.
So we'd have one system column per policy that's been applied to each
table. For users we'd need a catalog table to describe policies, and a
join table mapping users to policies with a user policy value.
Optimization by storing policy values for users directly in pg_role
might be worthwhile.
At the SQL level, an admin would then:
- Define policies
- Assign users labels in policies
- Apply policies to tables
PostgreSQL would be responsible for ensuring that rows written by a user
were labeled with that user's non-heirachical labels, and with their
current session level for their heirachical labels. It'd also be
responsible for enforcing matching on reads.
Internally, we can do all of this with the functoinality provided by
security-barrier views once write support is added. The interface
exposed to the user is made drastically easier to use, though.
Your example would become two non-heirachical security policies:
CREATE SECURITY POLICY colors AS (blue, red, purple);
CREATE SECURITY POLICY shapes AS (triangle, circle, square);
ALTER TABLE color_shapes ADD SECURITY POLICY colors;
ALTER TABLE colors_shapes ADD SECURITY POLICY shapes;
You'd assign users to the policies:
ALTER USER user1 SET SECURITY LABEL FOR POLICY colors TO blue, purple;
ALTER USER user2 SET SECURITY LABEL FOR POLICY colors TO red;
ALTER USER user1 SET SECURITY LABEL FOR POLICY shapes TO circle;
ALTER USER user2 SET SECURITY LABEL FOR POLICY shapes TO triangle;
user1 would now write rows with colors=blue|purple, shapes=circle. user2
would write rows with colors=red, shapes=triangle.
We'd provide session-level control over the current active label set, eg:
SET (LOCAL) LABELS FOR POLICY ... TO ...
so users can further control the rows they write and see at the session
level, but only within the limits of their authorized labels. Much like
what we have with SET ROLE at the moment. This would be particularly
crucial with heirachical policies, where rows get written at the current
session level.
I'll look at the rest of the mail a bit later; it's Saturday morning
here. I just wanted to raise this for your consideration now, as I think
this offers us a way to provide row-security that doesn't require every
user to just roll their own.
--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 12/14/2013 11:24 AM, Gregory Smith wrote:
Things I can already see to work on here are:
-Fix the regression tests
-Catch up to master again
I've got much of that in the tree:
https://github.com/ringerc/postgres/tree/rls-9.4
--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hi all
I'd like to outline a path toward getting row-security into core.
Comments / discussion appreciated.
I think I/we need to:
- Finish and commit updatable security barrier views. I've still got a
lot of straightening out to do there.
- Add an attribute to portals that stores the user ID at the time the
portal was planned. Possibly extensibly; I'd be surprised if we won't
need to associate other local info with a portal later.
- Allow storage of the pre-rewrite query plan and let saved plans be
marked as needing rewrite when replanned. We'll need this to permit some
users (initially just a hardcoded "superuser"; later by posession of a
certain right) to be totally exempt from row-security policy.
(Alternative: store separate with- and without-row-security plan trees
and pick which we use).
- Decide on and implement a structure for row-security functionality its
self. I'm persuaded by Robert's comments here, that whatever we expose
must be significantly more usable than a DIY on top of views (with the
fix for updatable security barrier views to make that possible). I
outlined the skeleton of a possible design in my earlier post, with the
heirachical and non-heirachical access labels. Should be implemented
using the same API we expose for extensions (like SEPostgresql RLS).
- Produce and commit a patch that adds the C API for row-security,
including calls to make it easy to wrap any relation in a dynamically
created or stored updatable security barrier subquery during rewrite.
- Produce and commit a patch series implementing the syntax, catalog
tables / catalog changes, documentation, etc for row-security that uses
this C API and commit it to core.
SEPostgreSQL RLS can then be built on top of the same API, using the
same core support.
Thoughts?
--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Craig Ringer <craig@2ndquadrant.com> writes:
- Add an attribute to portals that stores the user ID at the time the
portal was planned. Possibly extensibly; I'd be surprised if we won't
need to associate other local info with a portal later.
This bit seems rather confused. A portal is a runnable query; we
do not support replanning midstream, and I don't think we support
changes of UID either.
I agree that the plan cache needs to support treating change of UID
as a reason to discard a cached plan, but that's nothing to do with
portals.
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 12/16/2013 10:43 PM, Tom Lane wrote:
Craig Ringer <craig@2ndquadrant.com> writes:
- Add an attribute to portals that stores the user ID at the time the
portal was planned. Possibly extensibly; I'd be surprised if we won't
need to associate other local info with a portal later.This bit seems rather confused. A portal is a runnable query; we
do not support replanning midstream, and I don't think we support
changes of UID either.
We _do_ support changes of UID, or rather, current_user returns the
session user ID at the point in time it runs in the portal.
This can be observed with SECURITY DEFINER pl/pgsql functions returning
refcursor, and with cursors that're retained across a SET SESSION
AUTHORIZATION. They don't even need to be WITH HOLD, and s.s.a. can
occur within a transaction.
The point is to return the user ID at the time the portal was created,
rather than whatever the session now is.
--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 12/16/13 9:36 AM, Craig Ringer wrote:
- Finish and commit updatable security barrier views. I've still got a
lot of straightening out to do there.
I don't follow why you've put this part first. It has a lot of new
development and the risks that go along with that, but the POC projects
I've been testing are more interested in the view side issues.
- Decide on and implement a structure for row-security functionality its
self. I'm persuaded by Robert's comments here, that whatever we expose
must be significantly more usable than a DIY on top of views (with the
fix for updatable security barrier views to make that possible).
Can't say i agree we should be getting tangled into making this slick on
the user side right now. If you open a new can of worms like
heirachical access labels, this goes back into basic design, and we'll
spend months on that before returning to exactly the same implementation
details. I tried to make a case for how having a really generic
mechanism that's doesn't presume to know how labels will be assigned can
be a good thing.
Given the state this is all in right now, I'd much rather publish a hard
to use but powerful API than to presume you're going to get an easier to
use design right. The place I'm at here is trying to figure out the
simplest useful thing that could be committed and then hammer on the
details. (Not the first time I've beat that drum on a feature) Your
roadmap goes way past that, which is great to avoid being painted into a
corner, but I'm thinking more about what a useful feature freeze point
would look like given it's December 16 now.
--
Greg Smith greg.smith@crunchydatasolutions.com
Chief PostgreSQL Evangelist - http://crunchydatasolutions.com/
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Mon, Dec 16, 2013 at 3:12 PM, Gregory Smith <gregsmithpgsql@gmail.com> wrote:
On 12/16/13 9:36 AM, Craig Ringer wrote:
- Finish and commit updatable security barrier views. I've still got a
lot of straightening out to do there.I don't follow why you've put this part first. It has a lot of new
development and the risks that go along with that, but the POC projects I've
been testing are more interested in the view side issues.
I don't really see a way that any of this can work without that. To
be clear, that work is required even just for read-side security.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 16 December 2013 14:36, Craig Ringer <craig@2ndquadrant.com> wrote:
- Decide on and implement a structure for row-security functionality its
self. I'm persuaded by Robert's comments here, that whatever we expose
must be significantly more usable than a DIY on top of views (with the
fix for updatable security barrier views to make that possible). I
outlined the skeleton of a possible design in my earlier post, with the
heirachical and non-heirachical access labels. Should be implemented
using the same API we expose for extensions (like SEPostgresql RLS).
That part isn't clear why we "must" do better than that.
Having declarative security is a huge step forward, in just the same
way that updateable views were. They save the need for writing scripts
to implement things, rather than just having a useful default.
If there is a vision for that, lets see the vision. And then decide
whether its worth waiting for.
Personally, I see no reason not to commit the syntax we have now. So
people can see what we'll be supporting, whenever that is.
--
Simon Riggs http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 17 December 2013 17:03, Robert Haas <robertmhaas@gmail.com> wrote:
On Mon, Dec 16, 2013 at 3:12 PM, Gregory Smith <gregsmithpgsql@gmail.com> wrote:
On 12/16/13 9:36 AM, Craig Ringer wrote:
- Finish and commit updatable security barrier views. I've still got a
lot of straightening out to do there.I don't follow why you've put this part first. It has a lot of new
development and the risks that go along with that, but the POC projects I've
been testing are more interested in the view side issues.I don't really see a way that any of this can work without that. To
be clear, that work is required even just for read-side security.
Not sure I'd say required, but its certainly desirable to have
updateable security barrier views in themselves. And it comes across
to me as a cleaner and potentially more performant way of doing the
security checks for RLS. So I think its the right thing to do to wait
for this, even if we can't do that for 9.4
Realistically, we have a significant amount of work before we're ready
to pass a high security audit based around these features.
--
Simon Riggs http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 12/18/2013 01:03 AM, Robert Haas wrote:
On Mon, Dec 16, 2013 at 3:12 PM, Gregory Smith <gregsmithpgsql@gmail.com> wrote:
On 12/16/13 9:36 AM, Craig Ringer wrote:
- Finish and commit updatable security barrier views. I've still got a
lot of straightening out to do there.I don't follow why you've put this part first. It has a lot of new
development and the risks that go along with that, but the POC projects I've
been testing are more interested in the view side issues.I don't really see a way that any of this can work without that. To
be clear, that work is required even just for read-side security.
It's possible to build limited read-side-only security on top of the
existing s.b. views as they stand, with no update support.
You can grant write-only access to the base relations, and require
people to use a different relation name / schema when they want to
access a relation for write vs for read. You can't use RETURNING, and
you can still learn from result rowcounts etc. It's clumsy but usable-ish.
So it works - as long as you're using absolutely 100% read-only access
for users you need to constrain, or you don't mind explicitly referring
to the base table for write operations and not being able to use RETURNING.
I've been looking at write support primarily because I was under the
impression from prior discussion I read that the feature wasn't
considered committable as a read-only feature. If a consensus can be
built that read-only RLS would be acceptable after all, then I'll
happily defer that in favour of the other work items.
--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 12/18/2013 02:21 AM, Simon Riggs wrote:
On 16 December 2013 14:36, Craig Ringer <craig@2ndquadrant.com> wrote:
- Decide on and implement a structure for row-security functionality its
self. I'm persuaded by Robert's comments here, that whatever we expose
must be significantly more usable than a DIY on top of views (with the
fix for updatable security barrier views to make that possible). I
outlined the skeleton of a possible design in my earlier post, with the
heirachical and non-heirachical access labels. Should be implemented
using the same API we expose for extensions (like SEPostgresql RLS).That part isn't clear why we "must" do better than that.
Having declarative security is a huge step forward, in just the same
way that updateable views were. They save the need for writing scripts
to implement things, rather than just having a useful default.
In my view the proposed patch doesn't offer a significant improvement in
declarative security, beyond what we can get by just adding update
support to s.b. views and using search_path to control whether a user
sees the view or the base table.
It's a lot like Oracle Virtual Private Database (VPD): A set of low
level building blocks you can build your own flexible row security model
with. One where you have to very carefully write security-sensitive
predicates and define all your security model tables, etc yourself.
That's why I'm now of the opinion that we should make it possible to
achieve the same thing with s.b. views and the search_path (by adding
update support)... then build a declarative row-security system that
doesn't require the user fiddling with delicate queries and sensitive
scripts on top of that.
Personally, I see no reason not to commit the syntax we have now. So
people can see what we'll be supporting, whenever that is.
Do you mean commit the syntax without the supporting functionality, so
it doesn't operate / just ERROR's initially? Or are you suggesting that
the project commit the current patch with the known issues with user ID
changes and the concerns about its internal structure and operation,
then clean it up down the track?
If there is a vision for that, lets see the vision. And then decide
whether its worth waiting for.
OK, I'll put an example together as a follow-up to this post - the "how
it should look" part. Below I explain the *why* part.
What the current patch does is add something a bit like a simplified and
cut-down version of Oracle VPD.
Oracle's VPD predicates are generated by a PL/SQL procedure that emits
the SQL for a predicate. It has varying levels of caching to mitigate
performance effects. The proposed patch for Pg doesn't do that. We'll
have the predicate fixed and stored in the catalogs. It'll be like VPD
with policy level SHARED_STATIC locked in as the only option.
See
http://docs.oracle.com/cd/B28359_01/network.111/b28531/vpd.htm#DBSEG98250 for
details on Oracle VPD.
It's essentially a few tools you can build your own row security model
on, making the job a little easier. The DB designer / DBA still does a
lot of work to define security on each table and do a bunch of procedure
writing and setup to make it work. They have to get potentially security
sensitive queries just right for each table.
IMO it really isn't much different to just having an updatable security
barrier view. Differences arise when you look at VPD-exempt user acess
rights, at policy groups, etc, but we don't have any of that anyway.
The only big differences are that you can apply a VPD policy to an
existing table w/o having to refresh all views/functions that point to
it, and that some users can be made exempt.
(Note: I have not used VPD or Label Security myself; my comments are
intentionally based only on public documentation and on discussions with
others).
Oracle then built Oracle Label Security (their row-security solution)
*on* *top* of VPD. It's much more of a ready-to-go, configure it and use
it product, where you don't have to write procedures and define security
tables and generally build most of it yourself.
See: http://docs.oracle.com/cd/B14117_01/network.101/b10774.pdf
The Teradata implementation Robert pointed out skips the VPD-like layer
entirely. It just implements a fairly simple and flexible label security
model directly. You don't have to muck about with the details, you
don't have to deal with the guts of the row security implementation, you
just say "here's what I want".
I'd prefer to avoid adding syntax and interfaces for a feature that'll
likely become mostly implementation detail for the feature users
actually want - declarative security.
That's part of why I've been focused on upd. s. b. views. It'll provide
a way for users to do flexible DIY row-security without introducing a
big chunk of new syntax and functionality we'll be stuck with
supporting. It'll also be a useful building block for declarative row
security.
--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 2013-12-14 05:40, Craig Ringer wrote:
I find the heirachical and non-heirachical label security model used in
Teradata to be extremely interesting and worthy of study.The concept is that there are heirachical label policies (think
"classified", "unclassified", etc) or non-heirachical (mutually
exclusive labels). A user can see a row if they have all the policies
used in a table, and each of the user's policy values is sufficient to
see the row. For non-heiracical policies that means the user and row
must have at least one label in common; for heiracical policies it means
the user label must be greater than or equal to the row label.That model lets you quite flexibly and easily build complex security
models. It'd work well with PostgreSQL, too: We could implement
something like non-heirachical policies using a system varbit column
that's added whenever a non-heirachical policy gets added to a table,
and simply AND it with the varbit on the user's policy to see if they
can access the row. Or use a compact fixed-width bitfield. Heirachical
policies would use a 1 or 2 byte int system column to store the label.
Is is necessary to fix the attribute type of the security label? The
label model described above seems to restrictive to support e.g. the
labels described on p18 of the 'healthcare classification system' (HCS)
(http://www.hl7.org/documentcenter/public/wg/secure/3.%20HCS%20Guide%20Final%202013%200322%20JMD.pdf).
The HCS security label is based on the NIST's FIPS188 standard /
http://www.itl.nist.gov/fipspubs/fip188.htm
regards,
Yeb Havinga
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Tue, Dec 17, 2013 at 1:27 PM, Simon Riggs <simon@2ndquadrant.com> wrote:
Not sure I'd say required, but its certainly desirable to have
updateable security barrier views in themselves. And it comes across
to me as a cleaner and potentially more performant way of doing the
security checks for RLS.
Yes, that's how I'm thinking of it. It's required in the sense that
if we don't do it as a separate patch, we'll need to fold many of
changes into the RLS patch, which IMV is not desirable. We'd end up
with more complexity and less functionality with no real upside that I
can see.
But I think we are basically saying the same thing.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Wed, Dec 18, 2013 at 3:30 AM, Craig Ringer <craig@2ndquadrant.com> wrote:
In my view the proposed patch doesn't offer a significant improvement in
declarative security, beyond what we can get by just adding update
support to s.b. views and using search_path to control whether a user
sees the view or the base table.It's a lot like Oracle Virtual Private Database (VPD): A set of low
level building blocks you can build your own flexible row security model
with. One where you have to very carefully write security-sensitive
predicates and define all your security model tables, etc yourself.That's why I'm now of the opinion that we should make it possible to
achieve the same thing with s.b. views and the search_path (by adding
update support)... then build a declarative row-security system that
doesn't require the user fiddling with delicate queries and sensitive
scripts on top of that.
To be clear, I wasn't advocating for a declarative approach; I think
predicates are fine. There are usability issues to worry about either
way, and my concern is that we address those. A declarative approach
would certainly be valuable in that, for those people for whom it is
sufficiently flexible, it's probably quite a lot easier than writing
predicates. But I fear that some people will want a lot more
generality than a label-based system can accommodate.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 12/18/2013 11:14 PM, Robert Haas wrote:
To be clear, I wasn't advocating for a declarative approach; I think
predicates are fine. There are usability issues to worry about either
way, and my concern is that we address those. A declarative approach
would certainly be valuable in that, for those people for whom it is
sufficiently flexible, it's probably quite a lot easier than writing
predicates. But I fear that some people will want a lot more
generality than a label-based system can accommodate.
Having spent some time reading the HL7 spec, I now tend to agree that
labels alone are not going to be sufficient. There are security
decisions made based on:
- Security labels
- User/group/role memberships, both for accessor and target entity owner
(down to the row level)
- Black/white list ACLs, with inheritance and deny rules.
- One-off or narrow authorizations. "I permit my GP to examine my mental
health record details, but only over the last year, and the
authorization stands only for today."
- Authorization assertions. "I declare that the patient told me it is OK
to access these, let me see them."
- "Break glass" access. "This is an emergency. Give me access and I'll
deal with the consequences later."
So while security labels are an important part of the picture I'm forced
to agree that they are not sufficient, and that a generalized row
security mechanism actually is necessary. We don't have the time or
resources to build all these sorts of things individually, and if we did
it'd still be too inflexible for somebody.
In the end, sometimes I guess there's no replacement for "WHERE
call_some_procedure()"
In particular, labels are totally orthogonal to entity identity in the
data model, and being able to make row access decisions based on
information already in the data model is important. FK relationships to
owning entities and relationships between entities must be usable for
security access policy decisions.
So: I'm withdrawing the proposal to go straight to label security; I
concede that it's insufficiently expressive to meet all possible needs,
and we don't have the time to build anything declarative and
user-friendly that would be.
I do want to make sure that whatever we include this time around does
not create problems for a future label security approach, but that's
kind of required by the desire to add SEPostgreSQL RLS down the track
anyway.
Given that: What are your specific usability concerns about the current
approach proposed in KaiGai's patch?
My main worry was that it requires the user to build everything
manually, and is potentially error prone as a result. To address that we
can build convenience features (label security, ACL types and operators,
etc) on top of the same infrastructure later - it doesn't have to go in
right away.
So putting that side, the concerns I have are:
- The current RLS design is restricted to one predicate per table with
no contextual control over when that predicate applies. That means we
can't implement anything like "policy groups" or overlapping sets of
predicates, anything like that has to be built by the user. We don't
need multiple predicates to start with but I want to make sure there's
room for them later, particularly so that different apps / extensions
that use row-security don't have to fight with each other.
- You can't declare a predicate then apply it to a bunch of tables with
consistent security columns. Updating/changing predicates will be a
pain. We might be able to get around that by recommending that people
use an inlineable SQL function to declare their predicates, but
"inlineable" can be hard to pin down sometimes. If not, we need
something akin to GRANT ... ALL TABLES IN SCHEMA ... for row security,
and ALTER DEFAULT PRIVILEGES ... too.
- It's too hard to see what tables have row-security and what impact it
has. Needs psql improvements.
- There's no way to control whether or not a client can see the
row-security policy definition.
The other one I want to deal with is secure session variables, but
that's mostly a performance optimisation we can add later.
What's your list?
--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 12/18/13 10:21 PM, Craig Ringer wrote:
In the end, sometimes I guess there's no replacement for "WHERE
call_some_procedure()"
That's where I keep ending up at. The next round of examples I'm
reviewing this week plug pl/pgsql code into that model. And the one
after that actually references locally cached data that starts stored in
LDAP on another machine altogether. That one I haven't even asked for
permission to share with the community because of my long standing LDAP
allergy, but the whole thing plugs into the already submitted patch just
fine. (Shrug)
I started calling all of the things that generate data for RLS to filter
on "label providers". You've been using SELinux as an example future
label provider. Things like this LDAP originated bit are another
provider. Making the database itself a richer label provider one day is
an interesting usability improvement to map out. But on the proof of
concept things I've been getting passed I haven't seen an example where
I'd use that yet anyway. The real world label providers are too
complicated.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Wed, Dec 18, 2013 at 10:21 PM, Craig Ringer <craig@2ndquadrant.com> wrote:
My main worry was that it requires the user to build everything
manually, and is potentially error prone as a result. To address that we
can build convenience features (label security, ACL types and operators,
etc) on top of the same infrastructure later - it doesn't have to go in
right away.So putting that side, the concerns I have are:
- The current RLS design is restricted to one predicate per table with
no contextual control over when that predicate applies. That means we
can't implement anything like "policy groups" or overlapping sets of
predicates, anything like that has to be built by the user. We don't
need multiple predicates to start with but I want to make sure there's
room for them later, particularly so that different apps / extensions
that use row-security don't have to fight with each other.- You can't declare a predicate then apply it to a bunch of tables with
consistent security columns. Updating/changing predicates will be a
pain. We might be able to get around that by recommending that people
use an inlineable SQL function to declare their predicates, but
"inlineable" can be hard to pin down sometimes. If not, we need
something akin to GRANT ... ALL TABLES IN SCHEMA ... for row security,
and ALTER DEFAULT PRIVILEGES ... too.- It's too hard to see what tables have row-security and what impact it
has. Needs psql improvements.- There's no way to control whether or not a client can see the
row-security policy definition.The other one I want to deal with is secure session variables, but
that's mostly a performance optimisation we can add later.What's your list?
I don't have a lot of specific concerns apart from what I mentioned here:
/messages/by-id/CA+TgmoYC37qWNQkKQx3P3GU3Psfh3Tcox8uE06nnUiqn_4RAqA@mail.gmail.com
One thing we do need to think about is our good friend search_path,
and making sure that it's relatively easy to implement RLS in a way
that's secure against malicious manipulation thereof.
I do also agree with some of your concerns, particularly the first two
("is it too manual?" and "insufficient contextual control").
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 12/13/13 11:40 PM, Craig Ringer wrote:
You may want to check out the updated writable security-barrier views patch.
/messages/by-id/52AB112B.6020403@2ndquadrant.com
It may offer a path forward for the CF submission for RLS, letting us
get rid of the var/attr fiddling that many here objected to.
With my advocacy hat on, I'd like to revisit this idea now that there's
a viable updatable security barrier view submission. I thought the most
serious showstopper feedback from the last CF's RLS submission was that
this needed to be sorted out first. Reworking KaiGai's submission to
merge against Dean's new one makes it viable again in my mind, and I'd
like to continue toward re-reviewing it as part of this CF in that
light. Admittedly it's not ideal to try and do that at the same time
the barrier view patch is being modified, but I see that as a normal CF
merge of things based on other people's submissions.
I mentioned advocacy because the budding new PostgreSQL test instances
I'm seeing now will lose a lot of momentum if we end up with no user
visible RLS features in 9.4. The pieces we have now can assemble into
something that's useful, and I don't think that goal is unreasonably far
away.
--
Greg Smith greg.smith@crunchydatasolutions.com
Chief PostgreSQL Evangelist - http://crunchydatasolutions.com/
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 01/18/2014 03:27 AM, Gregory Smith wrote:
With my advocacy hat on, I'd like to revisit this idea now that there's
a viable updatable security barrier view submission. I thought the most
serious showstopper feedback from the last CF's RLS submission was that
this needed to be sorted out first. Reworking KaiGai's submission to
merge against Dean's new one makes it viable again in my mind, and I'd
like to continue toward re-reviewing it as part of this CF in that
light.
I had hoped to have this done weeks ago, but was blocked on getting a
viable approach to updatable security barrier views in place. I really
appreciate Dean, with his greater experience and skill in this area,
looking into it.
As it is I'm spending today reworking the RLS patch on top of the new
approach to updatable security barrier views.
Then it'll be a matter of tests, lots and lots of tests of every weird
corner I can think of.
Admittedly it's not ideal to try and do that at the same time
the barrier view patch is being modified, but I see that as a normal CF
merge of things based on other people's submissions.
I tend to agree - and the whole idea of reworking RLS on top of
updatable security barrier views is that it makes the patch for RLS's UI
and catalogs largely independent from the mechanics of filtering rows.
I mentioned advocacy because the budding new PostgreSQL test instances
I'm seeing now will lose a lot of momentum if we end up with no user
visible RLS features in 9.4. The pieces we have now can assemble into
something that's useful, and I don't think that goal is unreasonably far
away.
If it's possible, getting _something_ into 9.4 would be great. I'm aware
of multiple interested users who originally expected this in 9.3. That
hasn't worked out, but it'd be great to make 9.4.
--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 01/20/2014 09:58 AM, Craig Ringer wrote:
As it is I'm spending today reworking the RLS patch on top of the new
approach to updatable security barrier views.
To get that rolling I've split the RLS patch up into chunks, so we can
argue about the catalogs, ALTER syntax, and the actual row-filtering
implementation separately ;-)
It's currently on git@github.com:ringerc/postgres.git in the branch
rls-9.4-split, which is subject to rebasing. I'm still going through it
making sure each chunk at least compiles and preferably passes "make
check".
The first version is on the tag rls-9.4-split-v1, which will remain
static and contains an initial patch-split. The patch series for this
version is attached.
This is a clean tree on top of today's git master, it's not descended
from KaiGai's / Greg's trees. That means it doesn't track RLS's
development details, merge commits, etc. It's just a multi-stage patch
merge of RLS on top of master.
Hopefully it'll be a useful working point. If you omit "guts" commit:
RLS: Enforce row-security by transforming query plans
you'll get the skeleton of the catalogs and and supporting commands
without the actual planner/optimizer changes, forming the useful basis
for rebuilding RLS on top of Dean's updatable s.b. views patch.
I'm going through the patch series to make sure the split is consistent
and each piece at least builds by its self, then I'm going to rip out
the above-mentioned commit and rework it on top of the updatable
security barriers code.
--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
Attachments:
0001-RLS-Make-plan-caching-dependent-on-user-ID.patchtext/x-patch; name=0001-RLS-Make-plan-caching-dependent-on-user-ID.patchDownload
>From 5d8145dd0c47fb891139568f82ac87bc2403cfec Mon Sep 17 00:00:00 2001
From: Craig Ringer <craig@2ndquadrant.com>
Date: Mon, 20 Jan 2014 13:44:45 +0800
Subject: [PATCH 01/10] RLS: Make plan caching dependent on user ID.
---
src/backend/optimizer/plan/planner.c | 2 ++
src/backend/utils/cache/plancache.c | 32 ++++++++++++++++++++++++++++++++
src/include/nodes/plannodes.h | 2 ++
src/include/nodes/relation.h | 2 ++
src/include/utils/plancache.h | 2 ++
5 files changed, 40 insertions(+)
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 35bda67..8c12eb9 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -177,6 +177,7 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams)
glob->lastPHId = 0;
glob->lastRowMarkId = 0;
glob->transientPlan = false;
+ glob->planUserId = InvalidOid;
/* Determine what fraction of the plan is likely to be scanned */
if (cursorOptions & CURSOR_OPT_FAST_PLAN)
@@ -254,6 +255,7 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams)
result->relationOids = glob->relationOids;
result->invalItems = glob->invalItems;
result->nParamExec = glob->nParamExec;
+ result->planUserId = glob->planUserId;
return result;
}
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index d492cbb..77de2df 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -53,6 +53,7 @@
#include "catalog/namespace.h"
#include "executor/executor.h"
#include "executor/spi.h"
+#include "miscadmin.h"
#include "nodes/nodeFuncs.h"
#include "optimizer/cost.h"
#include "optimizer/planmain.h"
@@ -795,6 +796,16 @@ CheckCachedPlan(CachedPlanSource *plansource)
AcquireExecutorLocks(plan->stmt_list, true);
/*
+ * If plan was constructed with assumption of a particular user-id,
+ * and it is different from the current one, the cached-plan shall
+ * be invalidated to construct suitable query plan.
+ */
+ if (plan->is_valid &&
+ OidIsValid(plan->planUserId) &&
+ plan->planUserId == GetUserId())
+ plan->is_valid = false;
+
+ /*
* If plan was transient, check to see if TransactionXmin has
* advanced, and if so invalidate it.
*/
@@ -847,6 +858,8 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
{
CachedPlan *plan;
List *plist;
+ ListCell *cell;
+ Oid planUserId = InvalidOid;
bool snapshot_set;
bool spi_pushed;
MemoryContext plan_context;
@@ -914,6 +927,24 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
PopActiveSnapshot();
/*
+ * Check whether the generated plan assumes a particular user-id, or not.
+ * In case when a valid user-id is recorded on PlannedStmt->planUserId,
+ * it should be kept and used to validation check of the cached plan
+ * under the "current" user-id.
+ */
+ foreach (cell, plist)
+ {
+ PlannedStmt *pstmt = lfirst(cell);
+
+ if (IsA(pstmt, PlannedStmt) && OidIsValid(pstmt->planUserId))
+ {
+ Assert(!OidIsValid(planUserId) || planUserId == pstmt->planUserId);
+
+ planUserId = pstmt->planUserId;
+ }
+ }
+
+ /*
* Normally we make a dedicated memory context for the CachedPlan and its
* subsidiary data. (It's probably not going to be large, but just in
* case, use the default maxsize parameter. It's transient for the
@@ -956,6 +987,7 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
plan->is_oneshot = plansource->is_oneshot;
plan->is_saved = false;
plan->is_valid = true;
+ plan->planUserId = planUserId;
/* assign generation number to new plan */
plan->generation = ++(plansource->generation);
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 38c039c..35c88cc 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -67,6 +67,8 @@ typedef struct PlannedStmt
List *invalItems; /* other dependencies, as PlanInvalItems */
int nParamExec; /* number of PARAM_EXEC Params used */
+
+ Oid planUserId; /* user-id this plan assumed, or InvalidOid */
} PlannedStmt;
/* macro for fetching the Plan associated with a SubPlan node */
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 8aa40d0..74d0e12 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -99,6 +99,8 @@ typedef struct PlannerGlobal
Index lastRowMarkId; /* highest PlanRowMark ID assigned */
bool transientPlan; /* redo plan when TransactionXmin changes? */
+
+ Oid planUserId; /* User-Id to be assumed on this plan */
} PlannerGlobal;
/* macro for fetching the Plan associated with a SubPlan node */
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index b8ca643..ff89700 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -128,6 +128,8 @@ typedef struct CachedPlan
bool is_oneshot; /* is it a "oneshot" plan? */
bool is_saved; /* is CachedPlan in a long-lived context? */
bool is_valid; /* is the stmt_list currently valid? */
+ Oid planUserId; /* is user-id that is assumed on this cached
+ plan, or InvalidOid if portable for anybody */
TransactionId saved_xmin; /* if valid, replan when TransactionXmin
* changes from this value */
int generation; /* parent's generation number for this plan */
--
1.8.3.1
0002-RLS-add-pg_rowsecurity-catalog.patchtext/x-patch; name=0002-RLS-add-pg_rowsecurity-catalog.patchDownload
>From ea6c01b8370f789d75bfd0e3d5d23c63b5e92818 Mon Sep 17 00:00:00 2001
From: Craig Ringer <craig@2ndquadrant.com>
Date: Mon, 20 Jan 2014 13:07:50 +0800
Subject: [PATCH 02/10] RLS: add pg_rowsecurity catalog
---
doc/src/sgml/catalogs.sgml | 65 ++++++++
src/backend/catalog/Makefile | 4 +-
src/backend/catalog/dependency.c | 8 +
src/backend/catalog/heap.c | 1 +
src/backend/catalog/objectaddress.c | 50 ++++++
src/backend/catalog/pg_rowsecurity.c | 287 +++++++++++++++++++++++++++++++++++
src/backend/utils/cache/relcache.c | 17 ++-
src/include/catalog/dependency.h | 1 +
src/include/catalog/indexing.h | 5 +
src/include/catalog/pg_class.h | 25 +--
src/include/catalog/pg_rowsecurity.h | 74 +++++++++
src/include/utils/rel.h | 2 +
12 files changed, 525 insertions(+), 14 deletions(-)
create mode 100644 src/backend/catalog/pg_rowsecurity.c
create mode 100644 src/include/catalog/pg_rowsecurity.h
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 2230c93..1ca088a 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -234,6 +234,11 @@
</row>
<row>
+ <entry><link linkend="catalog-pg-rowsecurity"><structname>pg_rowsecuritylevelsec</structname></link></entry>
+ <entry>row-level security policy of relation</entry>
+ </row>
+
+ <row>
<entry><link linkend="catalog-pg-seclabel"><structname>pg_seclabel</structname></link></entry>
<entry>security labels on database objects</entry>
</row>
@@ -1876,6 +1881,16 @@
</row>
<row>
+ <entry><structfield>relhasrowsecurity</structfield></entry>
+ <entry><type>bool</type></entry>
+ <entry></entry>
+ <entry>
+ True if table has row-security policy; see
+ <link linkend="catalog-pg-rowsecurity"><structname>pg_rowsecurity</structname></link> catalog
+ </entry>
+ </row>
+
+ <row>
<entry><structfield>relhassubclass</structfield></entry>
<entry><type>bool</type></entry>
<entry></entry>
@@ -5157,6 +5172,56 @@
</sect1>
+ <sect1 id="catalog-pg-rowsecurity">
+ <title><structname>pg_rowsecurity</structname></title>
+
+ <indexterm zone="catalog-pg-rowsecurity">
+ <primary>pg_rowsecurity</primary>
+ </indexterm>
+ <para>
+ The catalog <structname>pg_rowsecurity</structname> stores expression
+ tree of row-security policy to be performed on a particular relation.
+ </para>
+ <table>
+ <title><structname>pg_rowsecurity</structname> Columns</title>
+ <tgroup cols="4">
+ <thead>
+ <row>
+ <entry>Name</entry>
+ <entry>Type</entry>
+ <entry>References</entry>
+ <entry>Description</entry>
+ </row>
+ </thead>
+ <tbody>
+ <row>
+ <entry><structfield>rsecrelid</structfield></entry>
+ <entry><type>oid</type></entry>
+ <entry><literal><link linkend="catalog-pg-class"><structname>pg_class</structname></link>.oid</literal></entry>
+ <entry>The table this row-security is for</entry>
+ </row>
+ <row>
+ <entry><structfield>rseccmd</structfield></entry>
+ <entry><type>char</type></entry>
+ <entry></entry>
+ <entry>The command this row-security is for. 'a' meaning all is only possible value right now.</entry>
+ </row>
+ <row>
+ <entry><structfield>rsecqual</structfield></entry>
+ <entry><type>pg_node_tree</type></entry>
+ <entry></entry>
+ <entry>An expression tree to be performed as row-security policy</entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+ <note>
+ <para>
+ <literal>pg_class.relhasrowsecurity</literal>
+ must be true if a table has row-security policy in this catalog.
+ </para>
+ </note>
+ </sect1>
<sect1 id="catalog-pg-seclabel">
<title><structname>pg_seclabel</structname></title>
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index a974bd5..beb73df 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -15,7 +15,7 @@ OBJS = catalog.o dependency.o heap.o index.o indexing.o namespace.o aclchk.o \
pg_constraint.o pg_conversion.o \
pg_depend.o pg_enum.o pg_inherits.o pg_largeobject.o pg_namespace.o \
pg_operator.o pg_proc.o pg_range.o pg_db_role_setting.o pg_shdepend.o \
- pg_type.o storage.o toasting.o
+ pg_rowsecurity.o pg_type.o storage.o toasting.o
BKIFILES = postgres.bki postgres.description postgres.shdescription
@@ -39,7 +39,7 @@ POSTGRES_BKI_SRCS = $(addprefix $(top_srcdir)/src/include/catalog/,\
pg_ts_config.h pg_ts_config_map.h pg_ts_dict.h \
pg_ts_parser.h pg_ts_template.h pg_extension.h \
pg_foreign_data_wrapper.h pg_foreign_server.h pg_user_mapping.h \
- pg_foreign_table.h \
+ pg_foreign_table.h pg_rowsecurity.h \
pg_default_acl.h pg_seclabel.h pg_shseclabel.h pg_collation.h pg_range.h \
toasting.h indexing.h \
)
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index e511669..8f95b14 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_rowsecurity.h"
#include "catalog/pg_tablespace.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_ts_config.h"
@@ -1249,6 +1250,10 @@ doDeletion(const ObjectAddress *object, int flags)
RemoveEventTriggerById(object->objectId);
break;
+ case OCLASS_ROWSECURITY:
+ RemoveRowSecurityById(object->objectId);
+ break;
+
default:
elog(ERROR, "unrecognized object class: %u",
object->classId);
@@ -2316,6 +2321,9 @@ getObjectClass(const ObjectAddress *object)
case EventTriggerRelationId:
return OCLASS_EVENT_TRIGGER;
+
+ case RowSecurityRelationId:
+ return OCLASS_ROWSECURITY;
}
/* shouldn't get here */
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 2cf4bc0..5be195b 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -799,6 +799,7 @@ InsertPgClassTuple(Relation pg_class_desc,
values[Anum_pg_class_relhaspkey - 1] = BoolGetDatum(rd_rel->relhaspkey);
values[Anum_pg_class_relhasrules - 1] = BoolGetDatum(rd_rel->relhasrules);
values[Anum_pg_class_relhastriggers - 1] = BoolGetDatum(rd_rel->relhastriggers);
+ values[Anum_pg_class_relhasrowsecurity - 1] = BoolGetDatum(rd_rel->relhasrowsecurity);
values[Anum_pg_class_relhassubclass - 1] = BoolGetDatum(rd_rel->relhassubclass);
values[Anum_pg_class_relispopulated - 1] = BoolGetDatum(rd_rel->relispopulated);
values[Anum_pg_class_relreplident - 1] = CharGetDatum(rd_rel->relreplident);
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 991c02a..870e477 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -2143,6 +2143,56 @@ getObjectDescription(const ObjectAddress *object)
break;
}
+ case OCLASS_ROWSECURITY:
+ {
+ Relation rsec_rel;
+ ScanKeyData skey;
+ SysScanDesc sscan;
+ HeapTuple tuple;
+ Form_pg_rowsecurity form_rsec;
+
+ rsec_rel = heap_open(RowSecurityRelationId, AccessShareLock);
+
+ ScanKeyInit(&skey,
+ ObjectIdAttributeNumber,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(object->objectId));
+ sscan = systable_beginscan(rsec_rel, RowSecurityOidIndexId,
+ true, NULL, 1, &skey);
+ tuple = systable_getnext(sscan);
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "could not find tuple for row-security %u",
+ object->objectId);
+ form_rsec = (Form_pg_rowsecurity) GETSTRUCT(tuple);
+
+ appendStringInfo(&buffer, _("row-security of "));
+ getRelationDescription(&buffer, form_rsec->rsecrelid);
+ switch (form_rsec->rseccmd)
+ {
+ case ROWSECURITY_CMD_ALL:
+ appendStringInfo(&buffer, _(" FOR ALL"));
+ break;
+ case ROWSECURITY_CMD_SELECT:
+ appendStringInfo(&buffer, _(" FOR SELECT"));
+ break;
+ case ROWSECURITY_CMD_INSERT:
+ appendStringInfo(&buffer, _(" FOR INSERT"));
+ break;
+ case ROWSECURITY_CMD_UPDATE:
+ appendStringInfo(&buffer, _(" FOR UPDATE"));
+ break;
+ case ROWSECURITY_CMD_DELETE:
+ appendStringInfo(&buffer, _(" FOR DELETE"));
+ break;
+ default:
+ elog(ERROR, "unexpected row-security command type: %c",
+ form_rsec->rseccmd);
+ }
+ systable_endscan(sscan);
+ heap_close(rsec_rel, AccessShareLock);
+ break;
+ }
+
default:
appendStringInfo(&buffer, "unrecognized object %u %u %d",
object->classId,
diff --git a/src/backend/catalog/pg_rowsecurity.c b/src/backend/catalog/pg_rowsecurity.c
new file mode 100644
index 0000000..7ee4e88
--- /dev/null
+++ b/src/backend/catalog/pg_rowsecurity.c
@@ -0,0 +1,287 @@
+/* -------------------------------------------------------------------------
+ *
+ * pg_rowsecurity.c
+ * routines to support manipulation of the pg_rowsecurity catalog
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * -------------------------------------------------------------------------
+ */
+#include "postgres.h"
+#include "access/genam.h"
+#include "access/heapam.h"
+#include "access/htup_details.h"
+#include "access/sysattr.h"
+#include "catalog/dependency.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_rowsecurity.h"
+#include "catalog/pg_type.h"
+#include "nodes/nodeFuncs.h"
+#include "optimizer/clauses.h"
+#include "parser/parse_clause.h"
+#include "parser/parse_node.h"
+#include "parser/parse_relation.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/inval.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/tqual.h"
+
+/*
+ * Load row-security policy from the catalog, and keep it on
+ * the relation cache.
+ */
+void
+RelationBuildRowSecurity(Relation relation)
+{
+ Relation catalog;
+ ScanKeyData skey;
+ SysScanDesc sscan;
+ HeapTuple tuple;
+ MemoryContext oldcxt;
+ MemoryContext rscxt = NULL;
+ RowSecurityDesc *rsdesc = NULL;
+
+ catalog = heap_open(RowSecurityRelationId, AccessShareLock);
+
+ ScanKeyInit(&skey,
+ Anum_pg_rowsecurity_rsecrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(RelationGetRelid(relation)));
+ sscan = systable_beginscan(catalog, RowSecurityRelidIndexId, true,
+ NULL, 1, &skey);
+ PG_TRY();
+ {
+ while (HeapTupleIsValid(tuple = systable_getnext(sscan)))
+ {
+ Datum value;
+ bool isnull;
+ char *temp;
+
+ if (!rsdesc)
+ {
+ rscxt = AllocSetContextCreate(CacheMemoryContext,
+ "Row-security descriptor",
+ ALLOCSET_SMALL_MINSIZE,
+ ALLOCSET_SMALL_INITSIZE,
+ ALLOCSET_SMALL_MAXSIZE);
+ oldcxt = MemoryContextSwitchTo(rscxt);
+ rsdesc = palloc0(sizeof(RowSecurityDesc));
+ rsdesc->rscxt = rscxt;
+ MemoryContextSwitchTo(oldcxt);
+ }
+ value = heap_getattr(tuple, Anum_pg_rowsecurity_rseccmd,
+ RelationGetDescr(catalog), &isnull);
+ Assert(!isnull);
+
+ if (DatumGetChar(value) != ROWSECURITY_CMD_ALL)
+ {
+ ereport(WARNING,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("Per-command row-security not implemented")));
+ continue;
+ }
+
+ value = heap_getattr(tuple, Anum_pg_rowsecurity_rsecqual,
+ RelationGetDescr(catalog), &isnull);
+ Assert(!isnull);
+ temp = TextDatumGetCString(value);
+
+ oldcxt = MemoryContextSwitchTo(rscxt);
+ rsdesc->rsall.rsecid = HeapTupleGetOid(tuple);
+ rsdesc->rsall.qual = (Expr *) stringToNode(temp);
+ Assert(exprType((Node *)rsdesc->rsall.qual) == BOOLOID);
+ rsdesc->rsall.hassublinks
+ = contain_subplans((Node *)rsdesc->rsall.qual);
+ MemoryContextSwitchTo(oldcxt);
+
+ pfree(temp);
+ }
+ }
+ PG_CATCH();
+ {
+ if (rscxt != NULL)
+ MemoryContextDelete(rscxt);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ systable_endscan(sscan);
+ heap_close(catalog, AccessShareLock);
+
+ relation->rsdesc = rsdesc;
+}
+
+/*
+ * Parse the supplied row-security policy, and insert/update a row
+ * of pg_rowsecurity catalog.
+ */
+static void
+InsertOrUpdatePolicyRow(Relation relation, char rseccmd, Node *clause)
+{
+ Oid relationId = RelationGetRelid(relation);
+ Oid rowsecId;
+ ParseState *pstate;
+ RangeTblEntry *rte;
+ Node *qual;
+ Relation catalog;
+ ScanKeyData skeys[2];
+ SysScanDesc sscan;
+ HeapTuple oldtup;
+ HeapTuple newtup;
+ Datum values[Natts_pg_rowsecurity];
+ bool isnull[Natts_pg_rowsecurity];
+ bool replaces[Natts_pg_rowsecurity];
+ ObjectAddress target;
+ ObjectAddress myself;
+
+ /* Parse the supplied clause */
+ pstate = make_parsestate(NULL);
+
+ rte = addRangeTableEntryForRelation(pstate, relation,
+ NULL, false, false);
+ addRTEtoQuery(pstate, rte, false, true, true);
+
+ qual = transformWhereClause(pstate, copyObject(clause),
+ EXPR_KIND_ROW_SECURITY,
+ "ROW SECURITY");
+ /* zero-clear */
+ memset(values, 0, sizeof(values));
+ memset(replaces, 0, sizeof(replaces));
+ memset(isnull, 0, sizeof(isnull));
+
+ /* Update or Insert an entry to pg_rowsecurity catalog */
+ catalog = heap_open(RowSecurityRelationId, RowExclusiveLock);
+
+ ScanKeyInit(&skeys[0],
+ Anum_pg_rowsecurity_rsecrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(RelationGetRelid(relation)));
+ ScanKeyInit(&skeys[1],
+ Anum_pg_rowsecurity_rseccmd,
+ BTEqualStrategyNumber, F_CHAREQ,
+ CharGetDatum(rseccmd));
+ sscan = systable_beginscan(catalog, RowSecurityRelidIndexId, true,
+ NULL, 2, skeys);
+ oldtup = systable_getnext(sscan);
+ if (HeapTupleIsValid(oldtup))
+ {
+ rowsecId = HeapTupleGetOid(oldtup);
+
+ replaces[Anum_pg_rowsecurity_rsecqual - 1] = true;
+ values[Anum_pg_rowsecurity_rsecqual - 1]
+ = CStringGetTextDatum(nodeToString(qual));
+
+ newtup = heap_modify_tuple(oldtup,
+ RelationGetDescr(catalog),
+ values, isnull, replaces);
+ simple_heap_update(catalog, &newtup->t_self, newtup);
+
+ deleteDependencyRecordsFor(RowSecurityRelationId, rowsecId, false);
+ }
+ else
+ {
+ values[Anum_pg_rowsecurity_rsecrelid - 1]
+ = ObjectIdGetDatum(relationId);
+ values[Anum_pg_rowsecurity_rseccmd - 1]
+ = CharGetDatum(rseccmd);
+ values[Anum_pg_rowsecurity_rsecqual - 1]
+ = CStringGetTextDatum(nodeToString(qual));
+ newtup = heap_form_tuple(RelationGetDescr(catalog),
+ values, isnull);
+ rowsecId = simple_heap_insert(catalog, newtup);
+ }
+ CatalogUpdateIndexes(catalog, newtup);
+
+ heap_freetuple(newtup);
+
+ /* records dependencies of row-security policy and relation/columns */
+ target.classId = RelationRelationId;
+ target.objectId = relationId;
+ target.objectSubId = 0;
+
+ myself.classId = RowSecurityRelationId;
+ myself.objectId = rowsecId;
+ myself.objectSubId = 0;
+
+ recordDependencyOn(&myself, &target, DEPENDENCY_AUTO);
+
+ recordDependencyOnExpr(&myself, qual, pstate->p_rtable,
+ DEPENDENCY_NORMAL);
+ free_parsestate(pstate);
+
+ systable_endscan(sscan);
+ heap_close(catalog, RowExclusiveLock);
+}
+
+/*
+ * Remove row-security policy row of pg_rowsecurity
+ */
+static void
+DeletePolicyRow(Relation relation, char rseccmd)
+{
+ Assert(rseccmd == ROWSECURITY_CMD_ALL);
+
+ if (relation->rsdesc)
+ {
+ ObjectAddress address;
+
+ address.classId = RowSecurityRelationId;
+ address.objectId = relation->rsdesc->rsall.rsecid;
+ address.objectSubId = 0;
+
+ performDeletion(&address, DROP_RESTRICT, 0);
+ }
+ else
+ {
+ /* Nothing to do here */
+ elog(INFO, "relation %s has no row-security policy, skipped",
+ RelationGetRelationName(relation));
+ }
+}
+
+/*
+ * Guts of row-security policy deletion.
+ */
+void
+RemoveRowSecurityById(Oid rowsecId)
+{
+ Relation catalog;
+ ScanKeyData skey;
+ SysScanDesc sscan;
+ HeapTuple tuple;
+ Relation rel;
+ Oid relid;
+
+ catalog = heap_open(RowSecurityRelationId, RowExclusiveLock);
+
+ ScanKeyInit(&skey,
+ ObjectIdAttributeNumber,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(rowsecId));
+ sscan = systable_beginscan(catalog, RowSecurityOidIndexId, true,
+ NULL, 1, &skey);
+ tuple = systable_getnext(sscan);
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "could not find tuple for row-security %u", rowsecId);
+
+ /*
+ * Open and exclusive-lock the relation the row-security belongs to.
+ */
+ relid = ((Form_pg_rowsecurity) GETSTRUCT(tuple))->rsecrelid;
+
+ rel = heap_open(relid, AccessExclusiveLock);
+
+ simple_heap_delete(catalog, &tuple->t_self);
+
+ /* Ensure relcache entries of other session being rebuilt */
+ CacheInvalidateRelcache(rel);
+
+ heap_close(rel, NoLock);
+
+ systable_endscan(sscan);
+ heap_close(catalog, RowExclusiveLock);
+}
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 2a46cfc..8ee5353 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -50,6 +50,7 @@
#include "catalog/pg_opclass.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_rewrite.h"
+#include "catalog/pg_rowsecurity.h"
#include "catalog/pg_tablespace.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
@@ -932,6 +933,11 @@ RelationBuildDesc(Oid targetRelId, bool insertIt)
else
relation->trigdesc = NULL;
+ if (relation->rd_rel->relhasrowsecurity)
+ RelationBuildRowSecurity(relation);
+ else
+ relation->rsdesc = NULL;
+
/*
* if it's an index, initialize index-related information
*/
@@ -1840,6 +1846,8 @@ RelationDestroyRelation(Relation relation)
MemoryContextDelete(relation->rd_indexcxt);
if (relation->rd_rulescxt)
MemoryContextDelete(relation->rd_rulescxt);
+ if (relation->rsdesc)
+ MemoryContextDelete(relation->rsdesc->rscxt);
if (relation->rd_fdwroutine)
pfree(relation->rd_fdwroutine);
pfree(relation);
@@ -3166,7 +3174,13 @@ RelationCacheInitializePhase3(void)
relation->rd_rel->relhastriggers = false;
restart = true;
}
-
+ if (relation->rd_rel->relhasrowsecurity && relation->rsdesc == NULL)
+ {
+ RelationBuildRowSecurity(relation);
+ if (relation->rsdesc == NULL)
+ relation->rd_rel->relhasrowsecurity = false;
+ restart = true;
+ }
/* Release hold on the relation */
RelationDecrementReferenceCount(relation);
@@ -4478,6 +4492,7 @@ load_relcache_init_file(bool shared)
rel->rd_rules = NULL;
rel->rd_rulescxt = NULL;
rel->trigdesc = NULL;
+ rel->rsdesc = NULL;
rel->rd_indexprs = NIL;
rel->rd_indpred = NIL;
rel->rd_exclops = NULL;
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index 8948589..de9323e 100644
--- a/src/include/catalog/dependency.h
+++ b/src/include/catalog/dependency.h
@@ -147,6 +147,7 @@ typedef enum ObjectClass
OCLASS_DEFACL, /* pg_default_acl */
OCLASS_EXTENSION, /* pg_extension */
OCLASS_EVENT_TRIGGER, /* pg_event_trigger */
+ OCLASS_ROWSECURITY, /* pg_rowsecurity */
MAX_OCLASS /* MUST BE LAST */
} ObjectClass;
diff --git a/src/include/catalog/indexing.h b/src/include/catalog/indexing.h
index 0515b75..4f46c5d 100644
--- a/src/include/catalog/indexing.h
+++ b/src/include/catalog/indexing.h
@@ -313,6 +313,11 @@ DECLARE_UNIQUE_INDEX(pg_extension_name_index, 3081, on pg_extension using btree(
DECLARE_UNIQUE_INDEX(pg_range_rngtypid_index, 3542, on pg_range using btree(rngtypid oid_ops));
#define RangeTypidIndexId 3542
+DECLARE_UNIQUE_INDEX(pg_rowsecurity_oid_index, 5001, on pg_rowsecurity using btree(oid oid_ops));
+#define RowSecurityOidIndexId 5001
+DECLARE_UNIQUE_INDEX(pg_rowsecurity_relid_index, 5002, on pg_rowsecurity using btree(rsecrelid oid_ops, rseccmd char_ops));
+#define RowSecurityRelidIndexId 5002
+
/* last step of initialization script: build the indexes declared above */
BUILD_INDICES
diff --git a/src/include/catalog/pg_class.h b/src/include/catalog/pg_class.h
index f2fb317..d4aa2f5 100644
--- a/src/include/catalog/pg_class.h
+++ b/src/include/catalog/pg_class.h
@@ -65,6 +65,7 @@ CATALOG(pg_class,1259) BKI_BOOTSTRAP BKI_ROWTYPE_OID(83) BKI_SCHEMA_MACRO
bool relhasrules; /* has (or has had) any rules */
bool relhastriggers; /* has (or has had) any TRIGGERs */
bool relhassubclass; /* has (or has had) derived classes */
+ bool relhasrowsecurity; /* has (or has had) row-security policy */
bool relispopulated; /* matview currently holds query results */
char relreplident; /* see REPLICA_IDENTITY_xxx constants */
TransactionId relfrozenxid; /* all Xids < this are frozen in this rel */
@@ -94,7 +95,7 @@ typedef FormData_pg_class *Form_pg_class;
* ----------------
*/
-#define Natts_pg_class 29
+#define Natts_pg_class 30
#define Anum_pg_class_relname 1
#define Anum_pg_class_relnamespace 2
#define Anum_pg_class_reltype 3
@@ -118,12 +119,13 @@ typedef FormData_pg_class *Form_pg_class;
#define Anum_pg_class_relhasrules 21
#define Anum_pg_class_relhastriggers 22
#define Anum_pg_class_relhassubclass 23
-#define Anum_pg_class_relispopulated 24
-#define Anum_pg_class_relreplident 25
-#define Anum_pg_class_relfrozenxid 26
-#define Anum_pg_class_relminmxid 27
-#define Anum_pg_class_relacl 28
-#define Anum_pg_class_reloptions 29
+#define Anum_pg_class_relhasrowsecurity 24
+#define Anum_pg_class_relispopulated 25
+#define Anum_pg_class_relreplident 26
+#define Anum_pg_class_relfrozenxid 27
+#define Anum_pg_class_relminmxid 28
+#define Anum_pg_class_relacl 29
+#define Anum_pg_class_reloptions 30
/* ----------------
* initial contents of pg_class
@@ -138,13 +140,14 @@ typedef FormData_pg_class *Form_pg_class;
* Note: "3" in the relfrozenxid column stands for FirstNormalTransactionId;
* similarly, "1" in relminmxid stands for FirstMultiXactId
*/
-DATA(insert OID = 1247 ( pg_type PGNSP 71 0 PGUID 0 0 0 0 0 0 0 f f p r 30 0 t f f f f t n 3 1 _null_ _null_ ));
+
+DATA(insert OID = 1247 ( pg_type PGNSP 71 0 PGUID 0 0 0 0 0 0 0 f f p r 29 0 t f f f f f t n 3 1 _null_ _null_ ));
DESCR("");
-DATA(insert OID = 1249 ( pg_attribute PGNSP 75 0 PGUID 0 0 0 0 0 0 0 f f p r 21 0 f f f f f t n 3 1 _null_ _null_ ));
+DATA(insert OID = 1249 ( pg_attribute PGNSP 75 0 PGUID 0 0 0 0 0 0 0 f f p r 21 0 f f f f f f t n 3 1 _null_ _null_ ));
DESCR("");
-DATA(insert OID = 1255 ( pg_proc PGNSP 81 0 PGUID 0 0 0 0 0 0 0 f f p r 27 0 t f f f f t n 3 1 _null_ _null_ ));
+DATA(insert OID = 1255 ( pg_proc PGNSP 81 0 PGUID 0 0 0 0 0 0 0 f f p r 27 0 t f f f f f t n 3 1 _null_ _null_ ));
DESCR("");
-DATA(insert OID = 1259 ( pg_class PGNSP 83 0 PGUID 0 0 0 0 0 0 0 f f p r 29 0 t f f f f t n 3 1 _null_ _null_ ));
+DATA(insert OID = 1259 ( pg_class PGNSP 83 0 PGUID 0 0 0 0 0 0 0 f f p r 30 0 t f f f f f t n 3 1 _null_ _null_ ));
DESCR("");
diff --git a/src/include/catalog/pg_rowsecurity.h b/src/include/catalog/pg_rowsecurity.h
new file mode 100644
index 0000000..10aed9a
--- /dev/null
+++ b/src/include/catalog/pg_rowsecurity.h
@@ -0,0 +1,74 @@
+/*
+ * pg_rowsecurity.h
+ * definition of the system catalog for row-security policy (pg_rowsecurity)
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ */
+#ifndef PG_ROWSECURITY_H
+#define PG_ROWSECURITY_H
+
+#include "catalog/genbki.h"
+#include "nodes/primnodes.h"
+#include "utils/memutils.h"
+#include "utils/relcache.h"
+
+/* ----------------
+ * pg_rowlevelsec definition. cpp turns this into
+ * typedef struct FormData_pg_rowlevelsec
+ * ----------------
+ */
+#define RowSecurityRelationId 5000
+
+CATALOG(pg_rowsecurity,5000)
+{
+ /* Oid of the relation that has row-security policy */
+ Oid rsecrelid;
+
+ /* One of ROWSECURITY_CMD_* below */
+ char rseccmd;
+#ifdef CATALOG_VARLEN
+ pg_node_tree rsecqual;
+#endif
+} FormData_pg_rowsecurity;
+
+/* ----------------
+ * Form_pg_rowlevelsec corresponds to a pointer to a row with
+ * the format of pg_rowlevelsec relation.
+ * ----------------
+ */
+typedef FormData_pg_rowsecurity *Form_pg_rowsecurity;
+
+/* ----------------
+ * compiler constants for pg_rowlevelsec
+ * ----------------
+ */
+#define Natts_pg_rowsecurity 3
+#define Anum_pg_rowsecurity_rsecrelid 1
+#define Anum_pg_rowsecurity_rseccmd 2
+#define Anum_pg_rowsecurity_rsecqual 3
+
+#define ROWSECURITY_CMD_ALL 'a'
+#define ROWSECURITY_CMD_SELECT 's'
+#define ROWSECURITY_CMD_INSERT 'i'
+#define ROWSECURITY_CMD_UPDATE 'u'
+#define ROWSECURITY_CMD_DELETE 'd'
+
+typedef struct
+{
+ Oid rsecid;
+ Expr *qual;
+ bool hassublinks;
+} RowSecurityEntry;
+
+typedef struct
+{
+ MemoryContext rscxt;
+ RowSecurityEntry rsall; /* row-security policy for ALL */
+} RowSecurityDesc;
+
+extern void RelationBuildRowSecurity(Relation relation);
+extern void RemoveRowSecurityById(Oid relationId);
+
+#endif /* PG_ROWSECURITY_H */
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 9b8a4c9..83a8d03 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -18,6 +18,7 @@
#include "catalog/pg_am.h"
#include "catalog/pg_class.h"
#include "catalog/pg_index.h"
+#include "catalog/pg_rowsecurity.h"
#include "fmgr.h"
#include "nodes/bitmapset.h"
#include "rewrite/prs2lock.h"
@@ -110,6 +111,7 @@ typedef struct RelationData
RuleLock *rd_rules; /* rewrite rules */
MemoryContext rd_rulescxt; /* private memory cxt for rd_rules, if any */
TriggerDesc *trigdesc; /* Trigger info, or NULL if rel has none */
+ RowSecurityDesc *rsdesc; /* Row-security policy, or NULL */
/*
* The index chosen as the relation's replication identity or
--
1.8.3.1
0003-RLS-Add-rowsec_relid-to-ResultRelInfo.patchtext/x-patch; name=0003-RLS-Add-rowsec_relid-to-ResultRelInfo.patchDownload
>From 6958d4dad091de0ab81696c229da49892046b084 Mon Sep 17 00:00:00 2001
From: Craig Ringer <craig@2ndquadrant.com>
Date: Mon, 20 Jan 2014 13:28:12 +0800
Subject: [PATCH 03/10] RLS: Add rowsec_relid to ResultRelInfo
This hunk may still be needed in the new approach to filter out rows that've
changed to no longer be visible due to user action r action of triggers;
we'd need to filter the result relation.
---
src/backend/nodes/copyfuncs.c | 1 +
src/backend/nodes/equalfuncs.c | 1 +
src/backend/nodes/outfuncs.c | 1 +
src/backend/nodes/readfuncs.c | 1 +
src/include/nodes/execnodes.h | 4 ++++
src/include/nodes/parsenodes.h | 8 +++++++-
6 files changed, 15 insertions(+), 1 deletion(-)
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index bb356d0..52bd8f5 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -1977,6 +1977,7 @@ _copyRangeTblEntry(const RangeTblEntry *from)
COPY_SCALAR_FIELD(relkind);
COPY_NODE_FIELD(subquery);
COPY_SCALAR_FIELD(security_barrier);
+ COPY_SCALAR_FIELD(rowsec_relid);
COPY_SCALAR_FIELD(jointype);
COPY_NODE_FIELD(joinaliasvars);
COPY_NODE_FIELD(functions);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 5908d9a..b9b0de2 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -2269,6 +2269,7 @@ _equalRangeTblEntry(const RangeTblEntry *a, const RangeTblEntry *b)
COMPARE_SCALAR_FIELD(relkind);
COMPARE_NODE_FIELD(subquery);
COMPARE_SCALAR_FIELD(security_barrier);
+ COMPARE_SCALAR_FIELD(rowsec_relid);
COMPARE_SCALAR_FIELD(jointype);
COMPARE_NODE_FIELD(joinaliasvars);
COMPARE_NODE_FIELD(functions);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 568c3b8..cfa4cb3 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -2376,6 +2376,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node)
case RTE_SUBQUERY:
WRITE_NODE_FIELD(subquery);
WRITE_BOOL_FIELD(security_barrier);
+ WRITE_OID_FIELD(rowsec_relid);
break;
case RTE_JOIN:
WRITE_ENUM_FIELD(jointype, JoinType);
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index c22acd5..335579f 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -1218,6 +1218,7 @@ _readRangeTblEntry(void)
case RTE_SUBQUERY:
READ_NODE_FIELD(subquery);
READ_BOOL_FIELD(security_barrier);
+ READ_OID_FIELD(rowsec_relid);
break;
case RTE_JOIN:
READ_ENUM_FIELD(jointype, JoinType);
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index a301a08..83637eb 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -308,6 +308,8 @@ typedef struct JunkFilter
* ConstraintExprs array of constraint-checking expr states
* junkFilter for removing junk attributes from tuples
* projectReturning for computing a RETURNING list
+ * rowSecurity for row-security checks
+ * rowSecParams param-list if row-security has SubLink
* ----------------
*/
typedef struct ResultRelInfo
@@ -329,6 +331,8 @@ typedef struct ResultRelInfo
List **ri_ConstraintExprs;
JunkFilter *ri_junkFilter;
ProjectionInfo *ri_projectReturning;
+ Node *ri_rowSecurity;
+ List *ri_rowSecParams;
} ResultRelInfo;
/* ----------------
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 846c31a..48c6d0f 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -31,7 +31,8 @@ typedef enum QuerySource
QSRC_PARSER, /* added by parse analysis (now unused) */
QSRC_INSTEAD_RULE, /* added by unconditional INSTEAD rule */
QSRC_QUAL_INSTEAD_RULE, /* added by conditional INSTEAD rule */
- QSRC_NON_INSTEAD_RULE /* added by non-INSTEAD rule */
+ QSRC_NON_INSTEAD_RULE, /* added by non-INSTEAD rule */
+ QSRC_ROW_SECURITY, /* added by row-security */
} QuerySource;
/* Sort ordering options for ORDER BY and CREATE INDEX */
@@ -741,6 +742,11 @@ typedef struct RangeTblEntry
*/
Query *subquery; /* the sub-query */
bool security_barrier; /* is from security_barrier view? */
+ Oid rowsec_relid; /* OID of the original relation, if this
+ * sub-query originated from row-security
+ * policy on the relation. Elsewhere, it
+ * should be InvalidOid.
+ */
/*
* Fields valid for a join RTE (else NULL/zero):
--
1.8.3.1
0004-Add-ALTER-TABLE-commands-for-row-security.patchtext/x-patch; name=0004-Add-ALTER-TABLE-commands-for-row-security.patchDownload
>From 6a482c0a9426dbfa25735bbbc888de9e8b5474b1 Mon Sep 17 00:00:00 2001
From: Craig Ringer <craig@2ndquadrant.com>
Date: Mon, 20 Jan 2014 13:37:47 +0800
Subject: [PATCH 04/10] Add ALTER TABLE commands for row security
---
doc/src/sgml/ref/alter_table.sgml | 38 +++++++++++++++++++++++++++
src/backend/catalog/pg_rowsecurity.c | 50 ++++++++++++++++++++++++++++++++++++
src/backend/commands/event_trigger.c | 1 +
src/backend/commands/tablecmds.c | 26 +++++++++++++++++++
src/backend/parser/gram.y | 24 +++++++++++++++++
src/backend/parser/parse_agg.c | 6 +++++
src/backend/parser/parse_expr.c | 3 +++
src/include/catalog/pg_rowsecurity.h | 2 ++
src/include/nodes/parsenodes.h | 2 ++
src/include/parser/parse_node.h | 3 ++-
10 files changed, 154 insertions(+), 1 deletion(-)
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 89649a2..79f37d8 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -69,6 +69,8 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable>
NOT OF
OWNER TO <replaceable class="PARAMETER">new_owner</replaceable>
SET TABLESPACE <replaceable class="PARAMETER">new_tablespace</replaceable>
+ SET ROW SECURITY FOR <replaceable class="PARAMETER">rowsec_command</replaceable> TO (<replaceable class="PARAMETER">condition</replaceable>)
+ RESET ROW SECURITY FOR <replaceable class="PARAMETER">rowsec_command</replaceable>
REPLICA IDENTITY {DEFAULT | USING INDEX <replaceable class="PARAMETER">index_name</replaceable> | FULL | NOTHING}
<phrase>and <replaceable class="PARAMETER">table_constraint_using_index</replaceable> is:</phrase>
@@ -76,6 +78,8 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable>
[ CONSTRAINT <replaceable class="PARAMETER">constraint_name</replaceable> ]
{ UNIQUE | PRIMARY KEY } USING INDEX <replaceable class="PARAMETER">index_name</replaceable>
[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+<phrase>and <replaceable class="PARAMETER">rowsec_command</replaceable> is:</phrase>
+ { ALL | SELECT | INSERT | UPDATE | DELETE }
</synopsis>
</refsynopsisdiv>
@@ -581,6 +585,26 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable>
</varlistentry>
<varlistentry>
+ <term><literal>SET ROW SECURITY FOR <replaceable class="PARAMETER">rowsec_command</replaceable> TO (<replaceable class="PARAMETER">condition</replaceable>)</literal></term>
+ <listitem>
+ <para>
+ This form set row-level security policy of the table.
+ Supplied <replaceable class="PARAMETER">condition</replaceable> performs
+ as if it is implicitly appended to the qualifiers of <literal>WHERE</literal>
+ clause, although mechanism guarantees to evaluate this condition earlier
+ than any other user given condition.
+ <literal>ALL</> is the only supported command type right now.
+ See also <xref linkend="ROW-SECURITY">.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>RESET ROW SECURITY FOR <replaceable class="PARAMETER">rowsec_command</replaceable></literal></term>
+ <listitem>
+ <para>
+ This form reset row-level security policy of the table, if exists.
+ <literal>ALL</> is the only supported command type right now.
<term><literal>REPLICA IDENTITY</literal></term>
<listitem>
<para>
@@ -840,6 +864,20 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable>
</listitem>
</varlistentry>
+ <varlistentry>
+ <term><replaceable class="PARAMETER">condition</replaceable></term>
+ <listitem>
+ <para>
+ An expression that returns a value of type boolean. Expect for a case
+ when queries are executed with superuser privilege, only rows for which
+ this expression returns true will be fetched, updated or deleted.
+ This expression can reference columns of the relation being configured.
+ Sub-queries can be contained within expression tree, unless referenced
+ relation recursively references the same relation.
+ </para>
+ </listitem>
+ </varlistentry>
+
</variablelist>
</refsect1>
diff --git a/src/backend/catalog/pg_rowsecurity.c b/src/backend/catalog/pg_rowsecurity.c
index 7ee4e88..34d33e8 100644
--- a/src/backend/catalog/pg_rowsecurity.c
+++ b/src/backend/catalog/pg_rowsecurity.c
@@ -285,3 +285,53 @@ RemoveRowSecurityById(Oid rowsecId)
systable_endscan(sscan);
heap_close(catalog, RowExclusiveLock);
}
+
+/*
+ * ALTER TABLE <name> SET ROW SECURITY (...) OR
+ * RESET ROW SECURITY
+ */
+void
+ATExecSetRowSecurity(Relation relation, const char *cmdname, Node *clause)
+{
+ Oid relid = RelationGetRelid(relation);
+ char rseccmd;
+
+ if (strcmp(cmdname, "all") == 0)
+ rseccmd = ROWSECURITY_CMD_ALL;
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("Row-security for \"%s\" is not implemented yet",
+ cmdname)));
+
+ if (clause != NULL)
+ {
+ InsertOrUpdatePolicyRow(relation, rseccmd, clause);
+
+ /*
+ * Also, turn on relhasrowsecurity, if not.
+ */
+ if (!RelationGetForm(relation)->relhasrowsecurity)
+ {
+ Relation class_rel = heap_open(RelationRelationId,
+ RowExclusiveLock);
+ HeapTuple tuple;
+
+ tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for relation %u", relid);
+
+ ((Form_pg_class) GETSTRUCT(tuple))->relhasrowsecurity = true;
+
+ simple_heap_update(class_rel, &tuple->t_self, tuple);
+ CatalogUpdateIndexes(class_rel, tuple);
+
+ heap_freetuple(tuple);
+ heap_close(class_rel, RowExclusiveLock);
+ }
+ }
+ else
+ DeletePolicyRow(relation, rseccmd);
+
+ CacheInvalidateRelcache(relation);
+}
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 59f0842..51a1c0f 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -995,6 +995,7 @@ EventTriggerSupportsObjectClass(ObjectClass objclass)
case OCLASS_USER_MAPPING:
case OCLASS_DEFACL:
case OCLASS_EXTENSION:
+ case OCLASS_ROWSECURITY:
return true;
case MAX_OCLASS:
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 26a4613..b941f7f 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -37,6 +37,7 @@
#include "catalog/pg_inherits_fn.h"
#include "catalog/pg_namespace.h"
#include "catalog/pg_opclass.h"
+#include "catalog/pg_rowsecurity.h"
#include "catalog/pg_tablespace.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
@@ -2789,6 +2790,8 @@ AlterTableGetLockLevel(List *cmds)
case AT_SetTableSpace: /* must rewrite heap */
case AT_DropNotNull: /* may change some SQL plans */
case AT_SetNotNull:
+ case AT_SetRowSecurity:
+ case AT_ResetRowSecurity:
case AT_GenericOptions:
case AT_AlterColumnGenericOptions:
cmd_lockmode = AccessExclusiveLock;
@@ -3164,6 +3167,8 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
case AT_DropInherit: /* NO INHERIT */
case AT_AddOf: /* OF */
case AT_DropOf: /* NOT OF */
+ case AT_SetRowSecurity:
+ case AT_ResetRowSecurity:
ATSimplePermissions(rel, ATT_TABLE);
/* These commands never recurse */
/* No command-specific prep needed */
@@ -3449,6 +3454,11 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab, Relation rel,
case AT_DropOf:
ATExecDropOf(rel, lockmode);
break;
+ case AT_SetRowSecurity:
+ ATExecSetRowSecurity(rel, cmd->name, (Node *) cmd->def);
+ break;
+ case AT_ResetRowSecurity:
+ ATExecSetRowSecurity(rel, cmd->name, NULL);
case AT_ReplicaIdentity:
ATExecReplicaIdentity(rel, (ReplicaIdentityStmt *) cmd->def, lockmode);
break;
@@ -7814,6 +7824,22 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
Assert(defaultexpr);
break;
+ case OCLASS_ROWSECURITY:
+ /*
+ * A row-level security policy can depend on a column in case
+ * when the policy clause references a particular column.
+ * Due to same reason why TRIGGER ... WHEN does not support
+ * to change column's type being referenced in clause, row-
+ * level security policy also does not support it.
+ */
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot alter type of a column used in a row-level security policy"),
+ errdetail("%s depends on column \"%s\"",
+ getObjectDescription(&foundObject),
+ colName)));
+ break;
+
case OCLASS_PROC:
case OCLASS_TYPE:
case OCLASS_CAST:
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 12a6beb..2bf0010 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -260,6 +260,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
%type <node> alter_table_cmd alter_type_cmd opt_collate_clause
replica_identity
%type <list> alter_table_cmds alter_type_cmds
+%type <str> row_security_cmd
%type <dbehavior> opt_drop_behavior
@@ -2191,6 +2192,23 @@ alter_table_cmd:
n->def = (Node *)$2;
$$ = (Node *)n;
}
+ /* ALTER TABLE <name> SET ROW SECURITY FOR <cmd> TO (<expr>) */
+ | SET ROW SECURITY FOR row_security_cmd TO '(' a_expr ')'
+ {
+ AlterTableCmd *n = makeNode(AlterTableCmd);
+ n->subtype = AT_SetRowSecurity;
+ n->name = $5;
+ n->def = (Node *) $8;
+ $$ = (Node *)n;
+ }
+ /* ALTER TABLE <name> RESET ROW SECURITY FOR <cmd> */
+ | RESET ROW SECURITY FOR row_security_cmd
+ {
+ AlterTableCmd *n = makeNode(AlterTableCmd);
+ n->subtype = AT_ResetRowSecurity;
+ n->name = $5;
+ n->def = NULL;
+ }
/* ALTER TABLE <name> REPLICA IDENTITY */
| REPLICA IDENTITY_P replica_identity
{
@@ -2301,6 +2319,12 @@ reloption_elem:
}
;
+row_security_cmd: ALL { $$ = "all"; }
+ | SELECT { $$ = "select"; }
+ | INSERT { $$ = "insert"; }
+ | UPDATE { $$ = "update"; }
+ | DELETE_P { $$ = "delete"; }
+ ;
/*****************************************************************************
*
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
index 9613e2a..333b557 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -333,6 +333,9 @@ transformAggregateCall(ParseState *pstate, Aggref *agg,
case EXPR_KIND_TRIGGER_WHEN:
err = _("aggregate functions are not allowed in trigger WHEN conditions");
break;
+ case EXPR_KIND_ROW_SECURITY:
+ err = _("aggregate functions are not allowed in row-security policy");
+ break;
/*
* There is intentionally no default: case here, so that the
@@ -662,6 +665,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
case EXPR_KIND_TRIGGER_WHEN:
err = _("window functions are not allowed in trigger WHEN conditions");
break;
+ case EXPR_KIND_ROW_SECURITY:
+ err = _("window functions are not allowed in row-security policy");
+ break;
/*
* There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 81c9338..e289e77 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -1462,6 +1462,7 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
case EXPR_KIND_OFFSET:
case EXPR_KIND_RETURNING:
case EXPR_KIND_VALUES:
+ case EXPR_KIND_ROW_SECURITY:
/* okay */
break;
case EXPR_KIND_CHECK_CONSTRAINT:
@@ -2643,6 +2644,8 @@ ParseExprKindName(ParseExprKind exprKind)
return "EXECUTE";
case EXPR_KIND_TRIGGER_WHEN:
return "WHEN";
+ case EXPR_KIND_ROW_SECURITY:
+ return "ROW SECURITY";
/*
* There is intentionally no default: case here, so that the
diff --git a/src/include/catalog/pg_rowsecurity.h b/src/include/catalog/pg_rowsecurity.h
index 10aed9a..798d556 100644
--- a/src/include/catalog/pg_rowsecurity.h
+++ b/src/include/catalog/pg_rowsecurity.h
@@ -69,6 +69,8 @@ typedef struct
} RowSecurityDesc;
extern void RelationBuildRowSecurity(Relation relation);
+extern void ATExecSetRowSecurity(Relation relation,
+ const char *cmdname, Node *clause);
extern void RemoveRowSecurityById(Oid relationId);
#endif /* PG_ROWSECURITY_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 48c6d0f..3a918bb 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -1322,6 +1322,8 @@ typedef enum AlterTableType
AT_AddOf, /* OF <type_name> */
AT_DropOf, /* NOT OF */
AT_ReplicaIdentity, /* REPLICA IDENTITY */
+ AT_SetRowSecurity, /* SET ROW SECURITY (...) */
+ AT_ResetRowSecurity, /* RESET ROW SECURITY */
AT_GenericOptions /* OPTIONS (...) */
} AlterTableType;
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index 85598e8..7218e60 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -63,7 +63,8 @@ typedef enum ParseExprKind
EXPR_KIND_INDEX_PREDICATE, /* index predicate */
EXPR_KIND_ALTER_COL_TRANSFORM, /* transform expr in ALTER COLUMN TYPE */
EXPR_KIND_EXECUTE_PARAMETER, /* parameter value in EXECUTE */
- EXPR_KIND_TRIGGER_WHEN /* WHEN condition in CREATE TRIGGER */
+ EXPR_KIND_TRIGGER_WHEN, /* WHEN condition in CREATE TRIGGER */
+ EXPR_KIND_ROW_SECURITY, /* ROW SECURITY policy for a table */
} ParseExprKind;
--
1.8.3.1
0005-RLS-pg_dump-support-for-dumping-rowsecurity-catalogs.patchtext/x-patch; name=0005-RLS-pg_dump-support-for-dumping-rowsecurity-catalogs.patchDownload
>From a3572eb6ff17e712d21109bc87361b30da6e699b Mon Sep 17 00:00:00 2001
From: Craig Ringer <craig@2ndquadrant.com>
Date: Mon, 20 Jan 2014 13:45:56 +0800
Subject: [PATCH 05/10] RLS: pg_dump support for dumping rowsecurity catalogs
---
src/bin/pg_dump/common.c | 4 +
src/bin/pg_dump/pg_backup_archiver.c | 1 +
src/bin/pg_dump/pg_dump.c | 151 ++++++++++++++++++++++++++++++++++-
src/bin/pg_dump/pg_dump.h | 13 ++-
src/bin/pg_dump/pg_dump_sort.c | 5 ++
5 files changed, 169 insertions(+), 5 deletions(-)
diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c
index 4d35ae5..fd9e6cd 100644
--- a/src/bin/pg_dump/common.c
+++ b/src/bin/pg_dump/common.c
@@ -244,6 +244,10 @@ getSchemaData(Archive *fout, int *numTablesPtr)
write_msg(NULL, "reading rewrite rules\n");
getRules(fout, &numRules);
+ if (g_verbose)
+ write_msg(NULL, "reading row-security policies\n");
+ getRowSecurity(fout, tblinfo, numTables);
+
*numTablesPtr = numTables;
return tblinfo;
}
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 7fc0288..6502d93 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3137,6 +3137,7 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, RestoreOptions *ropt, bool isDat
strcmp(te->desc, "INDEX") == 0 ||
strcmp(te->desc, "RULE") == 0 ||
strcmp(te->desc, "TRIGGER") == 0 ||
+ strcmp(te->desc, "ROW SECURITY") == 0 ||
strcmp(te->desc, "USER MAPPING") == 0)
{
/* these object types don't have separate owners */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 3862f05..6939852 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -250,6 +250,7 @@ static char *myFormatType(const char *typname, int32 typmod);
static void getBlobs(Archive *fout);
static void dumpBlob(Archive *fout, BlobInfo *binfo);
static int dumpBlobs(Archive *fout, void *arg);
+static void dumpRowSecurity(Archive *fout, RowSecurityInfo *rsinfo);
static void dumpDatabase(Archive *AH);
static void dumpEncoding(Archive *AH);
static void dumpStdStrings(Archive *AH);
@@ -2714,6 +2715,134 @@ dumpBlobs(Archive *fout, void *arg)
return 1;
}
+/*
+ * getRowSecurity
+ * get information about every row-security policy on a dumpable table
+ */
+void
+getRowSecurity(Archive *fout, TableInfo tblinfo[], int numTables)
+{
+ PQExpBuffer query = createPQExpBuffer();
+ PGresult *res;
+ RowSecurityInfo *rsinfo;
+ int i_oid;
+ int i_tableoid;
+ int i_rseccmd;
+ int i_rsecqual;
+ int i, j, ntups;
+
+ /* row-security is not supported prior to v9.4 */
+ if (fout->remoteVersion < 90400)
+ return;
+
+ for (i=0; i < numTables; i++)
+ {
+ TableInfo *tbinfo = &tblinfo[i];
+
+ if (!tbinfo->hasrowsec || !tbinfo->dobj.dump)
+ continue;
+
+ if (g_verbose)
+ write_msg(NULL, "reading row-security policy for table \"%s\"\n",
+ tbinfo->dobj.name);
+
+ /*
+ * select table schema to ensure regproc name is qualified if needed
+ */
+ selectSourceSchema(fout, tbinfo->dobj.namespace->dobj.name);
+
+ resetPQExpBuffer(query);
+
+ appendPQExpBuffer(query,
+ "SELECT oid, tableoid, s.rseccmd, "
+ "pg_get_expr(s.rsecqual, s.rsecrelid) AS rsecqual "
+ "FROM pg_catalog.pg_rowsecurity s "
+ "WHERE rsecrelid = '%u'",
+ tbinfo->dobj.catId.oid);
+ res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+
+ ntups = PQntuples(res);
+
+ i_oid = PQfnumber(res, "oid");
+ i_tableoid = PQfnumber(res, "tableoid");
+ i_rseccmd = PQfnumber(res, "rseccmd");
+ i_rsecqual = PQfnumber(res, "rsecqual");
+
+ rsinfo = pg_malloc(ntups * sizeof(RowSecurityInfo));
+ for (j=0; j < ntups; j++)
+ {
+ char namebuf[NAMEDATALEN + 1];
+
+ rsinfo[j].dobj.objType = DO_ROW_SECURITY;
+ rsinfo[j].dobj.catId.tableoid =
+ atooid(PQgetvalue(res, j, i_tableoid));
+ rsinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
+ AssignDumpId(&rsinfo[j].dobj);
+ snprintf(namebuf, sizeof(namebuf), "row-security of %s",
+ tbinfo->rolname);
+ rsinfo[j].dobj.name = namebuf;
+ rsinfo[j].dobj.namespace = tbinfo->dobj.namespace;
+ rsinfo[j].rstable = tbinfo;
+ rsinfo[j].rseccmd = pg_strdup(PQgetvalue(res, j, i_rseccmd));
+ rsinfo[j].rsecqual = pg_strdup(PQgetvalue(res, j, i_rsecqual));
+ }
+ PQclear(res);
+ }
+ destroyPQExpBuffer(query);
+}
+
+/*
+ * dumpRowSecurity
+ * dump the definition of the given row-security policy
+ */
+static void
+dumpRowSecurity(Archive *fout, RowSecurityInfo *rsinfo)
+{
+ TableInfo *tbinfo = rsinfo->rstable;
+ PQExpBuffer query;
+ PQExpBuffer delqry;
+ const char *cmd;
+
+ if (dataOnly || !tbinfo->hasrowsec)
+ return;
+
+ query = createPQExpBuffer();
+ delqry = createPQExpBuffer();
+ appendPQExpBuffer(query, "ALTER TABLE %s SET ROW SECURITY ",
+ fmtId(tbinfo->dobj.name));
+ appendPQExpBuffer(delqry, "ALTER TABLE %s RESET ROW SECURITY ",
+ fmtId(tbinfo->dobj.name));
+ if (strcmp(rsinfo->rseccmd, "a") == 0)
+ cmd = "ALL";
+ else if (strcmp(rsinfo->rseccmd, "s") == 0)
+ cmd = "SELECT";
+ else if (strcmp(rsinfo->rseccmd, "i") == 0)
+ cmd = "INSERT";
+ else if (strcmp(rsinfo->rseccmd, "u") == 0)
+ cmd = "UPDATE";
+ else if (strcmp(rsinfo->rseccmd, "d") == 0)
+ cmd = "DELETE";
+ else
+ {
+ write_msg(NULL, "unexpected command type: '%s'\n", rsinfo->rseccmd);
+ exit_nicely(1);
+ }
+ appendPQExpBuffer(query, "FOR %s TO %s;\n", cmd, rsinfo->rsecqual);
+ appendPQExpBuffer(delqry, "FOR %s;\n", cmd);
+
+ ArchiveEntry(fout, rsinfo->dobj.catId, rsinfo->dobj.dumpId,
+ rsinfo->dobj.name,
+ rsinfo->dobj.namespace->dobj.name,
+ NULL,
+ tbinfo->rolname, false,
+ "ROW SECURITY", SECTION_POST_DATA,
+ query->data, delqry->data, NULL,
+ NULL, 0,
+ NULL, NULL);
+
+ destroyPQExpBuffer(query);
+}
+
static void
binary_upgrade_set_type_oids_by_type_oid(Archive *fout,
PQExpBuffer upgrade_buffer,
@@ -4242,6 +4371,7 @@ getTables(Archive *fout, int *numTables)
int i_relhastriggers;
int i_relhasindex;
int i_relhasrules;
+ int i_relhasrowsec;
int i_relhasoids;
int i_relfrozenxid;
int i_toastoid;
@@ -4293,10 +4423,7 @@ getTables(Archive *fout, int *numTables)
"(%s c.relowner) AS rolname, "
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
- "c.relfrozenxid, tc.oid AS toid, "
- "tc.relfrozenxid AS tfrozenxid, "
- "c.relpersistence, c.relispopulated, "
- "c.relreplident, c.relpages, "
+ "c.relhasrowsecurity, "
"CASE WHEN c.reloftype <> 0 THEN c.reloftype::pg_catalog.regtype ELSE NULL END AS reloftype, "
"d.refobjid AS owning_tab, "
"d.refobjsubid AS owning_col, "
@@ -4332,6 +4459,7 @@ getTables(Archive *fout, int *numTables)
"(%s c.relowner) AS rolname, "
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"c.relfrozenxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"c.relpersistence, c.relispopulated, "
@@ -4371,6 +4499,7 @@ getTables(Archive *fout, int *numTables)
"(%s c.relowner) AS rolname, "
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"c.relfrozenxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"c.relpersistence, 't' as relispopulated, "
@@ -4408,6 +4537,7 @@ getTables(Archive *fout, int *numTables)
"(%s c.relowner) AS rolname, "
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"c.relfrozenxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"'p' AS relpersistence, 't' as relispopulated, "
@@ -4444,6 +4574,7 @@ getTables(Archive *fout, int *numTables)
"(%s c.relowner) AS rolname, "
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"c.relfrozenxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"'p' AS relpersistence, 't' as relispopulated, "
@@ -4480,6 +4611,7 @@ getTables(Archive *fout, int *numTables)
"(%s c.relowner) AS rolname, "
"c.relchecks, (c.reltriggers <> 0) AS relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"c.relfrozenxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"'p' AS relpersistence, 't' as relispopulated, "
@@ -4516,6 +4648,7 @@ getTables(Archive *fout, int *numTables)
"(%s relowner) AS rolname, "
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"0 AS relfrozenxid, "
"0 AS toid, "
"0 AS tfrozenxid, "
@@ -4552,6 +4685,7 @@ getTables(Archive *fout, int *numTables)
"(%s relowner) AS rolname, "
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"0 AS relfrozenxid, "
"0 AS toid, "
"0 AS tfrozenxid, "
@@ -4584,6 +4718,7 @@ getTables(Archive *fout, int *numTables)
"(%s relowner) AS rolname, "
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"0 AS relfrozenxid, "
"0 AS toid, "
"0 AS tfrozenxid, "
@@ -4611,6 +4746,7 @@ getTables(Archive *fout, int *numTables)
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, "
"'t'::bool AS relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"0 AS relfrozenxid, "
"0 AS toid, "
"0 AS tfrozenxid, "
@@ -4648,6 +4784,7 @@ getTables(Archive *fout, int *numTables)
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, "
"'t'::bool AS relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"0 as relfrozenxid, "
"0 AS toid, "
"0 AS tfrozenxid, "
@@ -4695,6 +4832,7 @@ getTables(Archive *fout, int *numTables)
i_relhastriggers = PQfnumber(res, "relhastriggers");
i_relhasindex = PQfnumber(res, "relhasindex");
i_relhasrules = PQfnumber(res, "relhasrules");
+ i_relhasrowsec = PQfnumber(res, "relhasrowsecurity");
i_relhasoids = PQfnumber(res, "relhasoids");
i_relfrozenxid = PQfnumber(res, "relfrozenxid");
i_toastoid = PQfnumber(res, "toid");
@@ -4744,6 +4882,7 @@ getTables(Archive *fout, int *numTables)
tblinfo[i].hasindex = (strcmp(PQgetvalue(res, i, i_relhasindex), "t") == 0);
tblinfo[i].hasrules = (strcmp(PQgetvalue(res, i, i_relhasrules), "t") == 0);
tblinfo[i].hastriggers = (strcmp(PQgetvalue(res, i, i_relhastriggers), "t") == 0);
+ tblinfo[i].hasrowsec = (strcmp(PQgetvalue(res, i, i_relhasrowsec), "t") == 0);
tblinfo[i].hasoids = (strcmp(PQgetvalue(res, i, i_relhasoids), "t") == 0);
tblinfo[i].relispopulated = (strcmp(PQgetvalue(res, i, i_relispopulated), "t") == 0);
tblinfo[i].relreplident = *(PQgetvalue(res, i, i_relreplident));
@@ -7861,6 +8000,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
NULL, 0,
dumpBlobs, NULL);
break;
+ case DO_ROW_SECURITY:
+ dumpRowSecurity(fout, (RowSecurityInfo *) dobj);
+ break;
case DO_PRE_DATA_BOUNDARY:
case DO_POST_DATA_BOUNDARY:
/* never dumped, nothing to do */
@@ -15147,6 +15289,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
case DO_TRIGGER:
case DO_EVENT_TRIGGER:
case DO_DEFAULT_ACL:
+ case DO_ROW_SECURITY:
/* Post-data objects: must come after the post-data boundary */
addObjectDependency(dobj, postDataBound->dumpId);
break;
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index c8dac35..de10c95 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -111,7 +111,8 @@ typedef enum
DO_PRE_DATA_BOUNDARY,
DO_POST_DATA_BOUNDARY,
DO_EVENT_TRIGGER,
- DO_REFRESH_MATVIEW
+ DO_REFRESH_MATVIEW,
+ DO_ROW_SECURITY,
} DumpableObjectType;
typedef struct _dumpableObject
@@ -245,6 +246,7 @@ typedef struct _tableInfo
bool hasindex; /* does it have any indexes? */
bool hasrules; /* does it have any rules? */
bool hastriggers; /* does it have any triggers? */
+ bool hasrowsec; /* does it have any row-security policy? */
bool hasoids; /* does it have OIDs? */
uint32 frozenxid; /* for restore frozen xid */
Oid toast_oid; /* for restore toast frozen xid */
@@ -484,6 +486,14 @@ typedef struct _blobInfo
char *blobacl;
} BlobInfo;
+typedef struct _rowSecurityInfo
+{
+ DumpableObject dobj;
+ TableInfo *rstable;
+ char *rseccmd;
+ char *rsecqual;
+} RowSecurityInfo;
+
/* global decls */
extern bool force_quotes; /* double-quotes for identifiers flag */
extern bool g_verbose; /* verbose flag */
@@ -575,5 +585,6 @@ extern DefaultACLInfo *getDefaultACLs(Archive *fout, int *numDefaultACLs);
extern void getExtensionMembership(Archive *fout, ExtensionInfo extinfo[],
int numExtensions);
extern EventTriggerInfo *getEventTriggers(Archive *fout, int *numEventTriggers);
+extern void getRowSecurity(Archive *fout, TableInfo tblinfo[], int numTables);
#endif /* PG_DUMP_H */
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index bda3535..718007d 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -1342,6 +1342,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
"BLOB DATA (ID %d)",
obj->dumpId);
return;
+ case DO_ROW_SECURITY:
+ snprintf(buf, bufsize,
+ "ROW-SECURITY POLICY (ID %d OID %u)",
+ obj->dumpId, obj->catId.oid);
+ return;
case DO_PRE_DATA_BOUNDARY:
snprintf(buf, bufsize,
"PRE-DATA BOUNDARY (ID %d)",
--
1.8.3.1
0006-RLS-psql-support-for-reporting-row-security-constrai.patchtext/x-patch; name=0006-RLS-psql-support-for-reporting-row-security-constrai.patchDownload
>From c3e0a70e1b3e77a9ab50d8062492e0d2e2a567a0 Mon Sep 17 00:00:00 2001
From: Craig Ringer <craig@2ndquadrant.com>
Date: Mon, 20 Jan 2014 13:45:24 +0800
Subject: [PATCH 06/10] RLS: psql support for reporting row security
constraints
---
src/bin/psql/describe.c | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 0d4b151..e99b72a 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2764,6 +2764,10 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys
appendPQExpBuffer(&buf,
",\n pg_catalog.obj_description(c.oid, 'pg_class') as \"%s\"",
gettext_noop("Description"));
+ if (pset.sversion >= 90300)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_get_expr(rs.rsecqual, c.oid) as \"%s\"",
+ gettext_noop("Row-security"));
}
appendPQExpBufferStr(&buf,
@@ -2773,6 +2777,9 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys
appendPQExpBufferStr(&buf,
"\n LEFT JOIN pg_catalog.pg_index i ON i.indexrelid = c.oid"
"\n LEFT JOIN pg_catalog.pg_class c2 ON i.indrelid = c2.oid");
+ if (verbose && pset.sversion >= 90300)
+ appendPQExpBuffer(&buf,
+ "\n LEFT JOIN pg_rowsecurity rs ON rs.rsecrelid = c.oid");
appendPQExpBufferStr(&buf, "\nWHERE c.relkind IN (");
if (showTables)
--
1.8.3.1
0007-RLS-Enforce-row-security-by-transforming-query-plans.patchtext/x-patch; name=0007-RLS-Enforce-row-security-by-transforming-query-plans.patchDownload
>From 668e14b2de0d78fe5722b227071151a68ed759a1 Mon Sep 17 00:00:00 2001
From: Craig Ringer <craig@2ndquadrant.com>
Date: Mon, 20 Jan 2014 13:49:56 +0800
Subject: [PATCH 07/10] RLS: Enforce row-security by transforming query plans
---
src/backend/executor/execMain.c | 27 +-
src/backend/nodes/copyfuncs.c | 2 +
src/backend/nodes/equalfuncs.c | 2 +
src/backend/nodes/nodeFuncs.c | 12 +-
src/backend/nodes/outfuncs.c | 2 +
src/backend/nodes/readfuncs.c | 1 +
src/backend/optimizer/plan/planner.c | 21 +-
src/backend/optimizer/prep/preptlist.c | 68 ++-
src/backend/optimizer/prep/prepunion.c | 95 +++-
src/backend/optimizer/util/Makefile | 3 +-
src/backend/optimizer/util/rowsecurity.c | 744 +++++++++++++++++++++++++++++++
src/backend/rewrite/rewriteHandler.c | 16 +
src/backend/utils/adt/ri_triggers.c | 13 +-
src/include/miscadmin.h | 1 +
src/include/nodes/nodeFuncs.h | 1 +
src/include/nodes/parsenodes.h | 4 +-
src/include/nodes/relation.h | 4 +
src/include/optimizer/rowsecurity.h | 27 ++
src/include/rewrite/rewriteHandler.h | 1 +
19 files changed, 1016 insertions(+), 28 deletions(-)
create mode 100644 src/backend/optimizer/util/rowsecurity.c
create mode 100644 src/include/optimizer/rowsecurity.h
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 6b5f198..0041c23 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -789,8 +789,9 @@ InitPlan(QueryDesc *queryDesc, int eflags)
foreach(l, plannedstmt->rowMarks)
{
PlanRowMark *rc = (PlanRowMark *) lfirst(l);
- Oid relid;
- Relation relation;
+ RangeTblEntry *rte = NULL;
+ Relation relation = NULL;
+ LOCKMODE lockmode = NoLock;
ExecRowMark *erm;
/* ignore "parent" rowmarks; they are irrelevant at runtime */
@@ -803,27 +804,33 @@ InitPlan(QueryDesc *queryDesc, int eflags)
case ROW_MARK_NOKEYEXCLUSIVE:
case ROW_MARK_SHARE:
case ROW_MARK_KEYSHARE:
- relid = getrelid(rc->rti, rangeTable);
- relation = heap_open(relid, RowShareLock);
+ rte = rt_fetch(rc->rti, rangeTable);
+ lockmode = RowShareLock;
break;
case ROW_MARK_REFERENCE:
- relid = getrelid(rc->rti, rangeTable);
- relation = heap_open(relid, AccessShareLock);
+ rte = rt_fetch(rc->rti, rangeTable);
+ lockmode = AccessShareLock;
break;
case ROW_MARK_COPY:
/* there's no real table here ... */
- relation = NULL;
break;
default:
elog(ERROR, "unrecognized markType: %d", rc->markType);
- relation = NULL; /* keep compiler quiet */
break;
}
/* Check that relation is a legal target for marking */
- if (relation)
+ if (rte)
+ {
+ if (rte->rtekind == RTE_RELATION)
+ relation = heap_open(rte->relid, lockmode);
+ else
+ {
+ Assert(rte->rtekind == RTE_SUBQUERY);
+ relation = heap_open(rte->rowsec_relid, lockmode);
+ }
CheckValidRowMarkRel(relation, rc->markType);
-
+ }
erm = (ExecRowMark *) palloc(sizeof(ExecRowMark));
erm->relation = relation;
erm->rti = rc->rti;
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 52bd8f5..3a8b60c 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -1936,6 +1936,7 @@ _copyAppendRelInfo(const AppendRelInfo *from)
COPY_SCALAR_FIELD(parent_relid);
COPY_SCALAR_FIELD(child_relid);
+ COPY_SCALAR_FIELD(child_result);
COPY_SCALAR_FIELD(parent_reltype);
COPY_SCALAR_FIELD(child_reltype);
COPY_NODE_FIELD(translated_vars);
@@ -2467,6 +2468,7 @@ _copyQuery(const Query *from)
COPY_SCALAR_FIELD(canSetTag);
COPY_NODE_FIELD(utilityStmt);
COPY_SCALAR_FIELD(resultRelation);
+ COPY_SCALAR_FIELD(sourceRelation);
COPY_SCALAR_FIELD(hasAggs);
COPY_SCALAR_FIELD(hasWindowFuncs);
COPY_SCALAR_FIELD(hasSubLinks);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index b9b0de2..cb2abf6 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -814,6 +814,7 @@ _equalAppendRelInfo(const AppendRelInfo *a, const AppendRelInfo *b)
{
COMPARE_SCALAR_FIELD(parent_relid);
COMPARE_SCALAR_FIELD(child_relid);
+ COMPARE_SCALAR_FIELD(child_result);
COMPARE_SCALAR_FIELD(parent_reltype);
COMPARE_SCALAR_FIELD(child_reltype);
COMPARE_NODE_FIELD(translated_vars);
@@ -849,6 +850,7 @@ _equalQuery(const Query *a, const Query *b)
COMPARE_SCALAR_FIELD(canSetTag);
COMPARE_NODE_FIELD(utilityStmt);
COMPARE_SCALAR_FIELD(resultRelation);
+ COMPARE_SCALAR_FIELD(sourceRelation);
COMPARE_SCALAR_FIELD(hasAggs);
COMPARE_SCALAR_FIELD(hasWindowFuncs);
COMPARE_SCALAR_FIELD(hasSubLinks);
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 123f2a6..fa8f040 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -1948,8 +1948,11 @@ query_tree_walker(Query *query,
return true;
if (walker((Node *) query->withCheckOptions, context))
return true;
- if (walker((Node *) query->returningList, context))
- return true;
+ if (!(flags & QTW_IGNORE_RETURNING))
+ {
+ if (walker((Node *) query->returningList, context))
+ return true;
+ }
if (walker((Node *) query->jointree, context))
return true;
if (walker(query->setOperations, context))
@@ -2686,7 +2689,10 @@ query_tree_mutator(Query *query,
MUTATE(query->targetList, query->targetList, List *);
MUTATE(query->withCheckOptions, query->withCheckOptions, List *);
- MUTATE(query->returningList, query->returningList, List *);
+ if (!(flags & QTW_IGNORE_RETURNING))
+ MUTATE(query->returningList, query->returningList, List *);
+ else
+ query->returningList = copyObject(query->returningList);
MUTATE(query->jointree, query->jointree, FromExpr *);
MUTATE(query->setOperations, query->setOperations, Node *);
MUTATE(query->havingQual, query->havingQual, Node *);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index cfa4cb3..500faeb 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -1925,6 +1925,7 @@ _outAppendRelInfo(StringInfo str, const AppendRelInfo *node)
WRITE_UINT_FIELD(parent_relid);
WRITE_UINT_FIELD(child_relid);
+ WRITE_UINT_FIELD(child_result);
WRITE_OID_FIELD(parent_reltype);
WRITE_OID_FIELD(child_reltype);
WRITE_NODE_FIELD(translated_vars);
@@ -2241,6 +2242,7 @@ _outQuery(StringInfo str, const Query *node)
appendStringInfoString(str, " :utilityStmt <>");
WRITE_INT_FIELD(resultRelation);
+ WRITE_INT_FIELD(sourceRelation);
WRITE_BOOL_FIELD(hasAggs);
WRITE_BOOL_FIELD(hasWindowFuncs);
WRITE_BOOL_FIELD(hasSubLinks);
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 335579f..884f45c 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -201,6 +201,7 @@ _readQuery(void)
READ_BOOL_FIELD(canSetTag);
READ_NODE_FIELD(utilityStmt);
READ_INT_FIELD(resultRelation);
+ READ_INT_FIELD(sourceRelation);
READ_BOOL_FIELD(hasAggs);
READ_BOOL_FIELD(hasWindowFuncs);
READ_BOOL_FIELD(hasSubLinks);
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 8c12eb9..63f0ec2 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -33,6 +33,7 @@
#include "optimizer/planmain.h"
#include "optimizer/planner.h"
#include "optimizer/prep.h"
+#include "optimizer/rowsecurity.h"
#include "optimizer/subselect.h"
#include "optimizer/tlist.h"
#include "parser/analyze.h"
@@ -406,6 +407,19 @@ subquery_planner(PlannerGlobal *glob, Query *parse,
expand_inherited_tables(root);
/*
+ * Apply row-security policy of the relation being referenced,
+ * if configured with either of built-in or extension's features.
+ * RangeTblEntry of the relation with row-security policy shall
+ * be replaced with a row-security subquery that has simple scan
+ * on the target relation with row-security policy qualifiers.
+ *
+ * This routine assumes PlannerInfo is already handled with
+ * expand_inherited_tables, thus, AppendRelInfo or PlanRowMark
+ * have valid information.
+ */
+ apply_row_security_policy(root);
+
+ /*
* Set hasHavingQual to remember if HAVING clause is present. Needed
* because preprocess_expression will reduce a constant-true condition to
* an empty qual list ... but "HAVING TRUE" is not a semantic no-op.
@@ -898,6 +912,8 @@ inheritance_planner(PlannerInfo *root)
ChangeVarNodes((Node *) subroot.parse, rti, newrti, 0);
ChangeVarNodes((Node *) subroot.rowMarks, rti, newrti, 0);
ChangeVarNodes((Node *) subroot.append_rel_list, rti, newrti, 0);
+ if (subroot.parse->sourceRelation == rti)
+ subroot.parse->sourceRelation = newrti;
rte = copyObject(rte);
subroot.parse->rtable = lappend(subroot.parse->rtable,
rte);
@@ -960,7 +976,10 @@ inheritance_planner(PlannerInfo *root)
root->init_plans = subroot.init_plans;
/* Build list of target-relation RT indexes */
- resultRelations = lappend_int(resultRelations, appinfo->child_relid);
+ resultRelations = lappend_int(resultRelations,
+ (appinfo->child_result > 0 ?
+ appinfo->child_result :
+ appinfo->child_relid));
/* Build lists of per-relation WCO and RETURNING targetlists */
if (parse->withCheckOptions)
diff --git a/src/backend/optimizer/prep/preptlist.c b/src/backend/optimizer/prep/preptlist.c
index ee773b8..5e4a010 100644
--- a/src/backend/optimizer/prep/preptlist.c
+++ b/src/backend/optimizer/prep/preptlist.c
@@ -37,8 +37,49 @@
static List *expand_targetlist(List *tlist, int command_type,
- Index result_relation, List *range_table);
+ Index result_relation, Index source_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 Var that references sub-queries being originated from regular
+ * relations with row-level security policy due to nature of sub-query
+ * that has no system-column.
+ */
+static AttrNumber
+lookup_varattno(AttrNumber attno, Index rt_index, List *rtables)
+{
+ RangeTblEntry *rte = rt_fetch(rt_index, rtables);
+
+ if (rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_SECURITY)
+ {
+ ListCell *cell;
+
+ foreach (cell, rte->subquery->targetList)
+ {
+ TargetEntry *tle = lfirst(cell);
+ Var *var;
+
+ if (IsA(tle->expr, Const))
+ continue;
+
+ var = (Var *) tle->expr;
+ Assert(IsA(var, Var));
+
+ if (var->varattno == attno)
+ return tle->resno;
+ }
+ elog(ERROR, "invalid attno %d on row-security subquery target-list",
+ attno);
+ }
+ return attno;
+}
/*
* preprocess_targetlist
@@ -51,6 +92,7 @@ preprocess_targetlist(PlannerInfo *root, List *tlist)
{
Query *parse = root->parse;
int result_relation = parse->resultRelation;
+ int source_relation = parse->sourceRelation;
List *range_table = parse->rtable;
CmdType command_type = parse->commandType;
ListCell *lc;
@@ -73,8 +115,12 @@ preprocess_targetlist(PlannerInfo *root, List *tlist)
* 10/94
*/
if (command_type == CMD_INSERT || command_type == CMD_UPDATE)
+ {
tlist = expand_targetlist(tlist, command_type,
- result_relation, range_table);
+ result_relation,
+ source_relation,
+ range_table);
+ }
/*
* Add necessary junk columns for rowmarked rels. These values are needed
@@ -96,7 +142,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,
@@ -112,7 +159,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,
@@ -195,7 +243,8 @@ preprocess_targetlist(PlannerInfo *root, List *tlist)
*/
static List *
expand_targetlist(List *tlist, int command_type,
- Index result_relation, List *range_table)
+ Index result_relation, Index source_relation,
+ List *range_table)
{
List *new_tlist = NIL;
ListCell *tlist_item;
@@ -218,6 +267,9 @@ expand_targetlist(List *tlist, int command_type,
numattrs = RelationGetNumberOfAttributes(rel);
+ if (source_relation == 0)
+ source_relation = result_relation;
+
for (attrno = 1; attrno <= numattrs; attrno++)
{
Form_pg_attribute att_tup = rel->rd_att->attrs[attrno - 1];
@@ -298,8 +350,10 @@ expand_targetlist(List *tlist, int command_type,
case CMD_UPDATE:
if (!att_tup->attisdropped)
{
- new_expr = (Node *) makeVar(result_relation,
- attrno,
+ new_expr = (Node *) makeVar(source_relation,
+ lookup_varattno(attrno,
+ source_relation,
+ range_table),
atttype,
atttypmod,
attcollation,
diff --git a/src/backend/optimizer/prep/prepunion.c b/src/backend/optimizer/prep/prepunion.c
index 52dcc72..9bd97e7 100644
--- a/src/backend/optimizer/prep/prepunion.c
+++ b/src/backend/optimizer/prep/prepunion.c
@@ -55,6 +55,7 @@ typedef struct
{
PlannerInfo *root;
AppendRelInfo *appinfo;
+ bool in_returning;
} adjust_appendrel_attrs_context;
static Plan *recurse_set_operations(Node *setOp, PlannerInfo *root,
@@ -1594,6 +1595,7 @@ adjust_appendrel_attrs(PlannerInfo *root, Node *node, AppendRelInfo *appinfo)
context.root = root;
context.appinfo = appinfo;
+ context.in_returning = false;
/*
* Must be prepared to start with a Query or a bare expression tree.
@@ -1605,10 +1607,29 @@ adjust_appendrel_attrs(PlannerInfo *root, Node *node, AppendRelInfo *appinfo)
newnode = query_tree_mutator((Query *) node,
adjust_appendrel_attrs_mutator,
(void *) &context,
- QTW_IGNORE_RC_SUBQUERIES);
+ QTW_IGNORE_RC_SUBQUERIES |
+ QTW_IGNORE_RETURNING);
+ /*
+ * Returning clause on the relation being replaced with row-
+ * security subquery shall be handled in a special way, because
+ * of no system columns on subquery.
+ * Var references to system column or whole-row reference need
+ * to be adjusted to reference pseudo columns on behalf of
+ * the underlying these columns, however, RETURNGIN clause is
+ * an exception because its Var nodes are evaluated towards
+ * the "raw" target relation, not a fetched tuple.
+ */
+ context.in_returning = true;
+ newnode->returningList = (List *)
+ expression_tree_mutator((Node *) newnode->returningList,
+ adjust_appendrel_attrs_mutator,
+ (void *) &context);
if (newnode->resultRelation == appinfo->parent_relid)
{
- newnode->resultRelation = appinfo->child_relid;
+ newnode->resultRelation = (appinfo->child_result > 0 ?
+ appinfo->child_result :
+ appinfo->child_relid);
+ newnode->sourceRelation = appinfo->child_relid;
/* Fix tlist resnos too, if it's inherited UPDATE */
if (newnode->commandType == CMD_UPDATE)
newnode->targetList =
@@ -1624,6 +1645,49 @@ adjust_appendrel_attrs(PlannerInfo *root, Node *node, AppendRelInfo *appinfo)
}
static Node *
+fixup_var_on_rowsec_subquery(RangeTblEntry *rte, Var *var)
+{
+ ListCell *cell;
+
+ Assert(rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_SECURITY);
+ /*
+ * In case when row-level security policy is applied on the referenced
+ * table, its RangeTblEntry (RTE_RELATION) is replaced with sub-query
+ * to filter out unprivileged rows of underlying relation.
+ * Even though reference to this sub-query should perform as if ones
+ * to real relations, system column has to be cared in special way
+ * due to the nature of sub-query.
+ * Target-entries that reference system columns should be added on
+ * rowlevelsec.c, so all we need to do here is looking up underlying
+ * target-list that can reference underlying system column, and fix-
+ * up varattno of the referencing Var node with resno of TargetEntry.
+ */
+ foreach (cell, rte->subquery->targetList)
+ {
+ TargetEntry *subtle = lfirst(cell);
+
+ if (IsA(subtle->expr, Var))
+ {
+ Var *subvar = (Var *) subtle->expr;
+ Var *newnode;
+
+ if (subvar->varattno == var->varattno)
+ {
+ newnode = copyObject(var);
+ newnode->varattno = subtle->resno;
+ return (Node *)newnode;
+ }
+ }
+ else
+ Assert(IsA(subtle->expr, Const));
+ }
+ elog(ERROR, "could not find pseudo column of %d, relid %u",
+ var->varattno, var->varno);
+ return NULL;
+}
+
+static Node *
adjust_appendrel_attrs_mutator(Node *node,
adjust_appendrel_attrs_context *context)
{
@@ -1638,8 +1702,12 @@ adjust_appendrel_attrs_mutator(Node *node,
if (var->varlevelsup == 0 &&
var->varno == appinfo->parent_relid)
{
- var->varno = appinfo->child_relid;
+ var->varno = (context->in_returning &&
+ appinfo->child_result > 0 ?
+ appinfo->child_result :
+ appinfo->child_relid);
var->varnoold = appinfo->child_relid;
+
if (var->varattno > 0)
{
Node *newnode;
@@ -1664,6 +1732,14 @@ adjust_appendrel_attrs_mutator(Node *node,
*/
if (OidIsValid(appinfo->child_reltype))
{
+ Query *parse = context->root->parse;
+ RangeTblEntry *rte = rt_fetch(appinfo->child_relid,
+ parse->rtable);
+ if (!context->in_returning &&
+ rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_SECURITY)
+ var = (Var *)fixup_var_on_rowsec_subquery(rte, var);
+
Assert(var->vartype == appinfo->parent_reltype);
if (appinfo->parent_reltype != appinfo->child_reltype)
{
@@ -1708,7 +1784,18 @@ adjust_appendrel_attrs_mutator(Node *node,
return (Node *) rowexpr;
}
}
- /* system attributes don't need any other translation */
+ else
+ {
+ Query *parse = context->root->parse;
+ RangeTblEntry *rte;
+
+ rte = rt_fetch(appinfo->child_relid, parse->rtable);
+
+ if (!context->in_returning &&
+ rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_SECURITY)
+ return fixup_var_on_rowsec_subquery(rte, var);
+ }
}
return (Node *) var;
}
diff --git a/src/backend/optimizer/util/Makefile b/src/backend/optimizer/util/Makefile
index c54d0a6..d42a285 100644
--- a/src/backend/optimizer/util/Makefile
+++ b/src/backend/optimizer/util/Makefile
@@ -13,6 +13,7 @@ top_builddir = ../../../..
include $(top_builddir)/src/Makefile.global
OBJS = clauses.o joininfo.o orclauses.o pathnode.o placeholder.o \
- plancat.o predtest.o relnode.o restrictinfo.o tlist.o var.o
+ plancat.o predtest.o relnode.o restrictinfo.o tlist.o var.o \
+ rowsecurity.o
include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/optimizer/util/rowsecurity.c b/src/backend/optimizer/util/rowsecurity.c
new file mode 100644
index 0000000..c2e5a49
--- /dev/null
+++ b/src/backend/optimizer/util/rowsecurity.c
@@ -0,0 +1,744 @@
+/*
+ * optimizer/util/rowsecurity.c
+ * Routines to support row-security feature
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ */
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "access/htup_details.h"
+#include "access/sysattr.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_inherits_fn.h"
+#include "catalog/pg_rowsecurity.h"
+#include "catalog/pg_type.h"
+#include "miscadmin.h"
+#include "nodes/makefuncs.h"
+#include "nodes/nodeFuncs.h"
+#include "nodes/plannodes.h"
+#include "optimizer/clauses.h"
+#include "optimizer/prep.h"
+#include "optimizer/rowsecurity.h"
+#include "parser/parsetree.h"
+#include "rewrite/rewriteHandler.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "tcop/utility.h"
+
+/* flags to pull row-security policy */
+#define RSEC_FLAG_HAS_SUBLINKS 0x0001
+
+/* hook to allow extensions to apply their own security policy */
+row_security_policy_hook_type row_security_policy_hook = NULL;
+
+/*
+ * make_pseudo_column
+ *
+ * It makes a target-entry node that references underlying column.
+ * Its tle->expr is usualy Var node, but may be Const for dummy NULL
+ * if the supplied attribute was already dropped.
+ */
+static TargetEntry *
+make_pseudo_column(RangeTblEntry *subrte, AttrNumber attnum)
+{
+ Expr *expr;
+ char *resname;
+
+ Assert(subrte->rtekind == RTE_RELATION && OidIsValid(subrte->relid));
+ if (attnum == InvalidAttrNumber)
+ {
+ expr = (Expr *) makeWholeRowVar(subrte, (Index) 1, 0, false);
+ resname = get_rel_name(subrte->relid);
+ }
+ else
+ {
+ HeapTuple tuple;
+ Form_pg_attribute attform;
+
+ tuple = SearchSysCache2(ATTNUM,
+ ObjectIdGetDatum(subrte->relid),
+ Int16GetDatum(attnum));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for attribute %d of relation %u",
+ attnum, subrte->relid);
+ attform = (Form_pg_attribute) GETSTRUCT(tuple);
+
+ if (attform->attisdropped)
+ {
+ char namebuf[NAMEDATALEN];
+
+ /* Insert NULL just for a placeholder of dropped column */
+ expr = (Expr *) makeConst(INT4OID,
+ -1,
+ InvalidOid,
+ sizeof(int32),
+ (Datum) 0,
+ true, /* isnull */
+ true); /* byval */
+ sprintf(namebuf, "dummy-%d", (int)attform->attnum);
+ resname = pstrdup(namebuf);
+ }
+ else
+ {
+ expr = (Expr *) makeVar((Index) 1,
+ attform->attnum,
+ attform->atttypid,
+ attform->atttypmod,
+ attform->attcollation,
+ 0);
+ resname = pstrdup(NameStr(attform->attname));
+ }
+ ReleaseSysCache(tuple);
+ }
+ return makeTargetEntry(expr, -1, resname, false);
+}
+
+/*
+ * lookup_pseudo_column
+ *
+ * It looks-up resource number of the target-entry relevant to the given
+ * Var-node that references the row-security subquery. If required column
+ * is not in the subquery's target-list, this function also adds new one
+ * and returns its resource number.
+ */
+static AttrNumber
+lookup_pseudo_column(PlannerInfo *root,
+ RangeTblEntry *rte, AttrNumber varattno)
+{
+ Query *subqry;
+ RangeTblEntry *subrte;
+ TargetEntry *subtle;
+ ListCell *cell;
+
+ Assert(rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_SECURITY);
+
+ subqry = rte->subquery;
+ foreach (cell, subqry->targetList)
+ {
+ subtle = lfirst(cell);
+
+ /*
+ * If referenced artifical column is already constructed on the
+ * target-list of row-security subquery, nothing to do any more.
+ */
+ if (IsA(subtle->expr, Var))
+ {
+ Var *subvar = (Var *)subtle->expr;
+
+ Assert(subvar->varno == 1);
+ if (subvar->varattno == varattno)
+ return subtle->resno;
+ }
+ }
+
+ /*
+ * OK, we don't have an artifical column relevant to the required ones,
+ * so let's create a new artifical column on demand.
+ */
+ subrte = rt_fetch((Index) 1, subqry->rtable);
+ subtle = make_pseudo_column(subrte, varattno);
+ subtle->resno = list_length(subqry->targetList) + 1;
+
+ subqry->targetList = lappend(subqry->targetList, subtle);
+ rte->eref->colnames = lappend(rte->eref->colnames,
+ makeString(pstrdup(subtle->resname)));
+ return subtle->resno;
+}
+
+/*
+ * fixup_varnode_walker
+ *
+ * It recursively fixes up references to the relation to be replaced by
+ * row-security sub-query, and adds pseudo columns relevant to the
+ * underlying system columns or whole row-reference on demand.
+ */
+typedef struct {
+ PlannerInfo *root;
+ int varlevelsup;
+ Index *vartrans;
+} fixup_varnode_context;
+
+static bool
+fixup_varnode_walker(Node *node, fixup_varnode_context *context)
+{
+ if (node == NULL)
+ return false;
+
+ if (IsA(node, Var))
+ {
+ Var *var = (Var *) node;
+ List *rtable = context->root->parse->rtable;
+ RangeTblEntry *rte;
+ ListCell *cell;
+
+ /*
+ * Ignore it, if Var node does not reference the Query currently
+ * we focus on.
+ */
+ if (var->varlevelsup != context->varlevelsup)
+ return false;
+
+ if (context->vartrans[var->varno] > 0)
+ {
+ Index rtindex_trans = context->vartrans[var->varno];
+
+ rte = rt_fetch(rtindex_trans, rtable);
+ Assert(rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_SECURITY);
+
+ var->varno = var->varnoold = rtindex_trans;
+ var->varattno = lookup_pseudo_column(context->root, rte,
+ var->varattno);
+ }
+ else
+ {
+ rte = rt_fetch(var->varno, rtable);
+ if (rte->rtekind == RTE_RELATION && rte->inh)
+ {
+ foreach (cell, context->root->append_rel_list)
+ {
+ AppendRelInfo *appinfo = lfirst(cell);
+ RangeTblEntry *child_rte;
+
+ if (appinfo->parent_relid != var->varno)
+ continue;
+
+ child_rte = rt_fetch(appinfo->child_relid, rtable);
+ if (child_rte->rtekind == RTE_SUBQUERY &&
+ child_rte->subquery->querySource == QSRC_ROW_SECURITY)
+ (void) lookup_pseudo_column(context->root,
+ child_rte,
+ var->varattno);
+ }
+ }
+ }
+ }
+ else if (IsA(node, RangeTblRef))
+ {
+ RangeTblRef *rtr = (RangeTblRef *) node;
+
+ if (context->varlevelsup == 0 &&
+ context->vartrans[rtr->rtindex] != 0)
+ rtr->rtindex = context->vartrans[rtr->rtindex];
+ }
+ else if (IsA(node, Query))
+ {
+ bool result;
+
+ context->varlevelsup++;
+ result = query_tree_walker((Query *) node,
+ fixup_varnode_walker,
+ (void *) context, 0);
+ context->varlevelsup--;
+
+ return result;
+ }
+ return expression_tree_walker(node,
+ fixup_varnode_walker,
+ (void *) context);
+}
+
+/*
+ * check_infinite_recursion
+ *
+ * It is a wrong row-security configuration, if we try to expand
+ * the relation inside of row-security subquery originated from
+ * same relation!
+ */
+static void
+check_infinite_recursion(PlannerInfo *root, Oid relid)
+{
+ PlannerInfo *parent = root->parent_root;
+
+ if (parent && parent->parse->querySource == QSRC_ROW_SECURITY)
+ {
+ RangeTblEntry *rte = rt_fetch(1, parent->parse->rtable);
+
+ Assert(rte->rtekind == RTE_RELATION && OidIsValid(rte->relid));
+
+ if (relid == rte->relid)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("infinite recursion detected for relation \"%s\"",
+ get_rel_name(relid))));
+ check_infinite_recursion(parent, relid);
+ }
+}
+
+/*
+ * expand_rtentry_with_policy
+ *
+ * It extends a range-table entry of row-security sub-query with supplied
+ * security policy, and append it on the parse->rtable.
+ * This sub-query contains pseudo columns that reference underlying
+ * regular columns (at least, references to system column or whole of
+ * table reference shall be added on demand), and simple scan on the
+ * target relation.
+ * Any Var nodes that referenced the relation pointed by rtindex shall
+ * be adjusted to reference this sub-query instead. walker
+ */
+static Index
+expand_rtentry_with_policy(PlannerInfo *root, Index rtindex,
+ Expr *qual, int flags)
+{
+ Query *parse = root->parse;
+ RangeTblEntry *rte = rt_fetch(rtindex, parse->rtable);
+ Query *subqry;
+ RangeTblEntry *subrte;
+ RangeTblRef *subrtr;
+ TargetEntry *subtle;
+ RangeTblEntry *newrte;
+ HeapTuple tuple;
+ AttrNumber nattrs;
+ AttrNumber attnum;
+ List *targetList = NIL;
+ List *colNameList = NIL;
+ PlanRowMark *rowmark;
+
+ Assert(rte->rtekind == RTE_RELATION && !rte->inh);
+
+ /* check recursion to prevent infinite loop */
+ check_infinite_recursion(root, rte->relid);
+
+ /* Expand views inside SubLink node */
+ if (flags & RSEC_FLAG_HAS_SUBLINKS)
+ QueryRewriteExpr((Node *)qual, list_make1_oid(rte->relid));
+
+ /*
+ * Construction of sub-query
+ */
+ subqry = (Query *) makeNode(Query);
+ subqry->commandType = CMD_SELECT;
+ subqry->querySource = QSRC_ROW_SECURITY;
+
+ subrte = copyObject(rte);
+ subqry->rtable = list_make1(subrte);
+
+ subrtr = makeNode(RangeTblRef);
+ subrtr->rtindex = 1;
+ subqry->jointree = makeFromExpr(list_make1(subrtr), (Node *) qual);
+ if (flags & RSEC_FLAG_HAS_SUBLINKS)
+ subqry->hasSubLinks = true;
+
+ /*
+ * Construction of TargetEntries that reference underlying columns.
+ */
+ tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(rte->relid));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for relation %u", rte->relid);
+ nattrs = ((Form_pg_class) GETSTRUCT(tuple))->relnatts;
+ ReleaseSysCache(tuple);
+
+ for (attnum = 1; attnum <= nattrs; attnum++)
+ {
+ subtle = make_pseudo_column(subrte, attnum);
+ subtle->resno = list_length(targetList) + 1;
+ Assert(subtle->resno == attnum);
+
+ targetList = lappend(targetList, subtle);
+ colNameList = lappend(colNameList,
+ makeString(pstrdup(subtle->resname)));
+ }
+ subqry->targetList = targetList;
+
+ /* Expand RengeTblEntry with this sub-query */
+ newrte = makeNode(RangeTblEntry);
+ newrte->rtekind = RTE_SUBQUERY;
+ newrte->subquery = subqry;
+ newrte->security_barrier = true;
+ newrte->rowsec_relid = rte->relid;
+ newrte->eref = makeAlias(get_rel_name(rte->relid), colNameList);
+
+ parse->rtable = lappend(parse->rtable, newrte);
+
+ /*
+ * Fix up PlanRowMark if needed, then add references to 'tableoid' and
+ * 'ctid' that shall be added to handle row-level locking.
+ * Also see preprocess_targetlist() that adds some junk attributes.
+ */
+ rowmark = get_plan_rowmark(root->rowMarks, rtindex);
+ if (rowmark)
+ {
+ if (rowmark->rti == rowmark->prti)
+ rowmark->rti = rowmark->prti = list_length(parse->rtable);
+ else
+ rowmark->rti = list_length(parse->rtable);
+
+ lookup_pseudo_column(root, newrte, SelfItemPointerAttributeNumber);
+ lookup_pseudo_column(root, newrte, TableOidAttributeNumber);
+ }
+ return list_length(parse->rtable);
+}
+
+/*
+ * pull_row_security_policy
+ *
+ * It pulls the configured row-security policy of both built-in and
+ * extensions. If any, it returns expression tree.
+ */
+static Expr *
+pull_row_security_policy(CmdType cmd, Relation relation, int *p_flags)
+{
+ Expr *quals = NULL;
+ int flags = 0;
+
+ /*
+ * Pull the row-security policy configured with built-in features,
+ * if unprivileged users. Please note that superuser can bypass it.
+ */
+ if (relation->rsdesc && !superuser())
+ {
+ RowSecurityDesc *rsdesc = relation->rsdesc;
+
+ quals = copyObject(rsdesc->rsall.qual);
+ if (rsdesc->rsall.hassublinks)
+ flags |= RSEC_FLAG_HAS_SUBLINKS;
+ }
+
+ /*
+ * Also, ask extensions whether they want to apply their own
+ * row-security policy. If both built-in and extension has
+ * their own policy, it shall be merged.
+ */
+ if (row_security_policy_hook)
+ {
+ List *temp;
+
+ temp = (*row_security_policy_hook)(cmd, relation);
+ if (temp != NIL)
+ {
+ if ((flags & RSEC_FLAG_HAS_SUBLINKS) == 0 &&
+ contain_subplans((Node *) temp))
+ flags |= RSEC_FLAG_HAS_SUBLINKS;
+
+ if (quals != NULL)
+ temp = lappend(temp, quals);
+
+ if (list_length(temp) == 1)
+ quals = (Expr *)list_head(temp);
+ else if (list_length(temp) > 1)
+ quals = makeBoolExpr(AND_EXPR, temp, -1);
+ }
+ }
+ *p_flags = flags;
+ return quals;
+}
+
+/*
+ * copy_row_security_policy
+ *
+ * It construct a row-security subquery instead of raw COPY TO statement,
+ * if target relation has a row-level security policy
+ */
+bool
+copy_row_security_policy(CopyStmt *stmt, Relation rel, List *attnums)
+{
+ Expr *quals;
+ int flags;
+ Query *parse;
+ RangeTblEntry *rte;
+ RangeTblRef *rtr;
+ TargetEntry *tle;
+ Var *var;
+ ListCell *cell;
+
+ if (stmt->is_from)
+ return false;
+
+ quals = pull_row_security_policy(CMD_SELECT, rel, &flags);
+ if (!quals)
+ return false;
+
+ parse = (Query *) makeNode(Query);
+ parse->commandType = CMD_SELECT;
+ parse->querySource = QSRC_ROW_SECURITY;
+
+ rte = makeNode(RangeTblEntry);
+ rte->rtekind = RTE_RELATION;
+ rte->relid = RelationGetRelid(rel);
+ rte->relkind = RelationGetForm(rel)->relkind;
+
+ foreach (cell, attnums)
+ {
+ HeapTuple tuple;
+ Form_pg_attribute attform;
+ AttrNumber attno = lfirst_int(cell);
+
+ tuple = SearchSysCache2(ATTNUM,
+ ObjectIdGetDatum(RelationGetRelid(rel)),
+ Int16GetDatum(attno));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for attribute %d of relation %s",
+ attno, RelationGetRelationName(rel));
+ attform = (Form_pg_attribute) GETSTRUCT(tuple);
+
+ var = makeVar((Index) 1,
+ attform->attnum,
+ attform->atttypid,
+ attform->atttypmod,
+ attform->attcollation,
+ 0);
+ tle = makeTargetEntry((Expr *) var,
+ list_length(parse->targetList) + 1,
+ pstrdup(NameStr(attform->attname)),
+ false);
+ parse->targetList = lappend(parse->targetList, tle);
+
+ ReleaseSysCache(tuple);
+
+ rte->selectedCols = bms_add_member(rte->selectedCols,
+ attno - FirstLowInvalidHeapAttributeNumber);
+ }
+ rte->inFromCl = true;
+ rte->requiredPerms = ACL_SELECT;
+
+ rtr = makeNode(RangeTblRef);
+ rtr->rtindex = 1;
+
+ parse->jointree = makeFromExpr(list_make1(rtr), (Node *) quals);
+ parse->rtable = list_make1(rte);
+ if (flags & RSEC_FLAG_HAS_SUBLINKS)
+ parse->hasSubLinks = true;
+
+ stmt->query = (Node *) parse;
+
+ return true;
+}
+
+/*
+ * apply_row_security_relation
+ *
+ * It applies row-security policy on a particular relation being specified.
+ * If this relation is top of the inheritance tree, it also checks inherited
+ * children.
+ */
+static bool
+apply_row_security_relation(PlannerInfo *root, Index *vartrans,
+ CmdType cmd, Index rtindex)
+{
+ Query *parse = root->parse;
+ RangeTblEntry *rte = rt_fetch(rtindex, parse->rtable);
+ Relation rel;
+ Expr *qual;
+ int flags;
+ bool result = false;
+
+ if (!rte->inh)
+ {
+ rel = heap_open(rte->relid, NoLock);
+ qual = pull_row_security_policy(cmd, rel, &flags);
+ if (qual)
+ {
+ vartrans[rtindex]
+ = expand_rtentry_with_policy(root, rtindex, qual, flags);
+ if (parse->resultRelation == rtindex)
+ parse->sourceRelation = vartrans[rtindex];
+ result = true;
+ }
+ heap_close(rel, NoLock);
+ }
+ else
+ {
+ /*
+ * In case when relation has inherited children, we try to apply
+ * row-level security policy of them if configured.
+ * In addition to regular replacement with a sub-query, we need
+ * to adjust rtindex of AppendRelInfo and varno of translated_vars.
+ * It makes sub-queries perform like regular relations being
+ * inherited from a particular parent relation. So, a table scan
+ * may have underlying a relation scan and two sub-query scans for
+ * instance. If it is result relation of UPDATE or DELETE command,
+ * rtindex to the original relation (regular relation) has to be
+ * kept because sub-query cannot perform as an updatable relation.
+ * So, we save it on child_result of AppendRelInfo; that shall be
+ * used to track relations to be modified at inheritance_planner().
+ */
+ ListCell *lc1, *lc2;
+
+ foreach (lc1, root->append_rel_list)
+ {
+ AppendRelInfo *apinfo = lfirst(lc1);
+
+ if (apinfo->parent_relid != rtindex)
+ continue;
+
+ if (apply_row_security_relation(root, vartrans, cmd,
+ apinfo->child_relid))
+ {
+ /*
+ * Save the rtindex of actual relation to be modified,
+ * if parent relation is result relation of this query.
+ */
+ if (parse->resultRelation == rtindex)
+ apinfo->child_result = apinfo->child_relid;
+
+ apinfo->child_relid = vartrans[apinfo->child_relid];
+ /* Adjust varno to reference pseudo columns */
+ foreach (lc2, apinfo->translated_vars)
+ {
+ Var *var = lfirst(lc2);
+
+ if (var)
+ var->varno = apinfo->child_relid;
+ }
+ result = true;
+ }
+ }
+ }
+ return result;
+}
+
+/*
+ * apply_row_security_recursive
+ *
+ * It walks on the given join-tree to replace relations with row-level
+ * security policy by a simple sub-query.
+ */
+static bool
+apply_row_security_recursive(PlannerInfo *root, Index *vartrans, Node *jtnode)
+{
+ bool result = false;
+
+ if (jtnode == NULL)
+ return false;
+ if (IsA(jtnode, RangeTblRef))
+ {
+ Index rtindex = ((RangeTblRef *) jtnode)->rtindex;
+ Query *parse = root->parse;
+ RangeTblEntry *rte = rt_fetch(rtindex, parse->rtable);
+ CmdType cmd;
+
+ /* Only relation can have row-security policy */
+ if (rte->rtekind != RTE_RELATION)
+ return false;
+
+ /*
+ * Prevents infinite recursion. Please note that rtindex == 1
+ * of the row-security subquery is a relation being already
+ * processed on the upper level.
+ */
+ if (parse->querySource == QSRC_ROW_SECURITY && rtindex == 1)
+ return false;
+
+ /* Is it a result relation of UPDATE or DELETE command? */
+ if (parse->resultRelation == rtindex)
+ cmd = parse->commandType;
+ else
+ cmd = CMD_SELECT;
+
+ /* Try to apply row-security policy, if configured */
+ result = apply_row_security_relation(root, vartrans, cmd, rtindex);
+ }
+ else if (IsA(jtnode, FromExpr))
+ {
+ FromExpr *f = (FromExpr *) jtnode;
+ ListCell *l;
+
+ foreach (l, f->fromlist)
+ {
+ if (apply_row_security_recursive(root, vartrans, lfirst(l)))
+ result = true;
+ }
+ }
+ else if (IsA(jtnode, JoinExpr))
+ {
+ JoinExpr *j = (JoinExpr *) jtnode;
+
+ if (apply_row_security_recursive(root, vartrans, j->larg))
+ result = true;
+ if (apply_row_security_recursive(root, vartrans, j->rarg))
+ result = true;
+ }
+ else
+ elog(ERROR, "unexpected node type: %d", (int) nodeTag(jtnode));
+
+ return result;
+}
+
+/*
+ * apply_row_security_policy
+ *
+ * Entrypoint to apply configured row-security policy of the relation.
+ *
+ * In case when the supplied query references relations with row-security
+ * policy, its RangeTblEntry shall be replaced by a row-security subquery
+ * that has simple scan on the referenced table with policy qualifiers.
+ * Of course, security-barrier shall be set on the subquery to prevent
+ * unexpected push-down of functions without leakproof flag.
+ *
+ * For example, when table t1 has a security policy "(x % 2 = 0)", the
+ * following query:
+ * SELECT * FROM t1 WHERE f_leak(y)
+ * performs as if
+ * SELECT * FROM (
+ * SELECT x, y FROM t1 WHERE (x % 2 = 0)
+ * ) AS t1 WHERE f_leak(y)
+ * would be given. Because the sub-query has security barrier flag,
+ * configured security policy qualifier is always executed prior to
+ * user given functions.
+ */
+void
+apply_row_security_policy(PlannerInfo *root)
+{
+ Query *parse = root->parse;
+ Oid curr_userid;
+ int curr_seccxt;
+ Index *vartrans;
+
+ /*
+ * Mode checks. In case when SECURITY_ROW_LEVEL_DISABLED is set,
+ * no row-level security policy should be applied regardless
+ * whether it is built-in or extension.
+ */
+ GetUserIdAndSecContext(&curr_userid, &curr_seccxt);
+ if (curr_seccxt & SECURITY_ROW_LEVEL_DISABLED)
+ return;
+
+ vartrans = palloc0(sizeof(Index) * (list_length(parse->rtable) + 1));
+ if (apply_row_security_recursive(root, vartrans, (Node *)parse->jointree))
+ {
+ PlannerGlobal *glob = root->glob;
+ PlanInvalItem *pi_item;
+ fixup_varnode_context context;
+
+ /*
+ * Constructed Plan with row-level security policy depends on
+ * properties of current user (database superuser can bypass
+ * configured row-security policy!), thus, it has to be
+ * invalidated when its assumption was changed.
+ */
+ if (!OidIsValid(glob->planUserId))
+ {
+ /* Plan invalidation on session user-id */
+ glob->planUserId = GetUserId();
+
+ /* Plan invalidation on catalog updates of pg_authid */
+ pi_item = makeNode(PlanInvalItem);
+ pi_item->cacheId = AUTHOID;
+ pi_item->hashValue =
+ GetSysCacheHashValue1(AUTHOID,
+ ObjectIdGetDatum(glob->planUserId));
+ glob->invalItems = lappend(glob->invalItems, pi_item);
+ }
+ else
+ Assert(glob->planUserId == GetUserId());
+
+ /*
+ * Var-nodes that referenced RangeTblEntry to be replaced by
+ * row-security sub-query have to be adjusted for appropriate
+ * reference to the underlying pseudo column of the relation.
+ */
+ context.root = root;
+ context.varlevelsup = 0;
+ context.vartrans = vartrans;
+ query_tree_walker(parse,
+ fixup_varnode_walker,
+ (void *) &context,
+ QTW_IGNORE_RETURNING);
+ }
+ pfree(vartrans);
+}
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index 0b13645..2f56211 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -3282,3 +3282,19 @@ QueryRewrite(Query *parsetree)
return results;
}
+
+/*
+ * QueryRewriteExpr
+ *
+ * This routine provides an entry point of query rewriter towards
+ * a certain expression tree with SubLink node; being added after
+ * the top level query rewrite.
+ * It primarily intends to expand views appeared in the qualifiers
+ * appended with row-level security which needs to modify query
+ * tree at head of the planner stage.
+ */
+void
+QueryRewriteExpr(Node *node, List *activeRIRs)
+{
+ fireRIRonSubLink(node, activeRIRs);
+}
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index 1e1e616..68be040 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -3008,6 +3008,7 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
int spi_result;
Oid save_userid;
int save_sec_context;
+ int temp_sec_context;
Datum vals[RI_MAX_NUMKEYS * 2];
char nulls[RI_MAX_NUMKEYS * 2];
@@ -3087,8 +3088,18 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
/* Switch to proper UID to perform check as */
GetUserIdAndSecContext(&save_userid, &save_sec_context);
+
+ /*
+ * Row-level security should be disabled in case when foreign-key
+ * relation is queried to check existence of tuples that references
+ * the primary-key being modified.
+ */
+ temp_sec_context = save_sec_context | SECURITY_LOCAL_USERID_CHANGE;
+ if (source_is_pk)
+ temp_sec_context |= SECURITY_ROW_LEVEL_DISABLED;
+
SetUserIdAndSecContext(RelationGetForm(query_rel)->relowner,
- save_sec_context | SECURITY_LOCAL_USERID_CHANGE);
+ temp_sec_context);
/* Finally we can run the query. */
spi_result = SPI_execute_snapshot(qplan,
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index f133e5f..1c97bd9 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -272,6 +272,7 @@ extern int trace_recovery(int trace_level);
/* flags to be OR'd to form sec_context */
#define SECURITY_LOCAL_USERID_CHANGE 0x0001
#define SECURITY_RESTRICTED_OPERATION 0x0002
+#define SECURITY_ROW_LEVEL_DISABLED 0x0004
extern char *DatabasePath;
diff --git a/src/include/nodes/nodeFuncs.h b/src/include/nodes/nodeFuncs.h
index f9afdb6..9ed3796 100644
--- a/src/include/nodes/nodeFuncs.h
+++ b/src/include/nodes/nodeFuncs.h
@@ -24,6 +24,7 @@
#define QTW_IGNORE_RANGE_TABLE 0x08 /* skip rangetable entirely */
#define QTW_EXAMINE_RTES 0x10 /* examine RTEs */
#define QTW_DONT_COPY_QUERY 0x20 /* do not copy top Query */
+#define QTW_IGNORE_RETURNING 0x40 /* skip returning clause */
extern Oid exprType(const Node *expr);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 3a918bb..c8ce32a 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -113,7 +113,9 @@ typedef struct Query
int resultRelation; /* rtable index of target relation for
* INSERT/UPDATE/DELETE; 0 for SELECT */
-
+ int sourceRelation; /* rtable index of source relation for
+ * UPDATE/DELETE, if not identical with
+ * resultRelation; 0 for elsewhere */
bool hasAggs; /* has aggregates in tlist or havingQual */
bool hasWindowFuncs; /* has window functions in tlist */
bool hasSubLinks; /* has subquery SubLink */
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 74d0e12..252e45f 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -1438,6 +1438,10 @@ typedef struct AppendRelInfo
*/
Index parent_relid; /* RT index of append parent rel */
Index child_relid; /* RT index of append child rel */
+ Index child_result; /* RT index of append child rel's source,
+ * if source of result relation is not
+ * identical. Elsewhere, 0.
+ */
/*
* For an inheritance appendrel, the parent and child are both regular
diff --git a/src/include/optimizer/rowsecurity.h b/src/include/optimizer/rowsecurity.h
new file mode 100644
index 0000000..ff4dd9a
--- /dev/null
+++ b/src/include/optimizer/rowsecurity.h
@@ -0,0 +1,27 @@
+/* -------------------------------------------------------------------------
+ *
+ * rowsecurity.h
+ * prototypes for optimizer/rowsecurity.c
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * -------------------------------------------------------------------------
+ */
+#ifndef ROWSECURITY_H
+#define ROWSECURITY_H
+
+#include "nodes/execnodes.h"
+#include "nodes/parsenodes.h"
+#include "nodes/relation.h"
+#include "utils/rel.h"
+
+typedef List *(*row_security_policy_hook_type)(CmdType cmdtype,
+ Relation relation);
+extern PGDLLIMPORT row_security_policy_hook_type row_security_policy_hook;
+
+extern bool copy_row_security_policy(CopyStmt *stmt,
+ Relation relation, List *attnums);
+extern void apply_row_security_policy(PlannerInfo *root);
+
+#endif /* ROWSECURITY_H */
diff --git a/src/include/rewrite/rewriteHandler.h b/src/include/rewrite/rewriteHandler.h
index e4027bd..7bd334c 100644
--- a/src/include/rewrite/rewriteHandler.h
+++ b/src/include/rewrite/rewriteHandler.h
@@ -18,6 +18,7 @@
#include "nodes/parsenodes.h"
extern List *QueryRewrite(Query *parsetree);
+extern void QueryRewriteExpr(Node *node, List *activeRIRs);
extern void AcquireRewriteLocks(Query *parsetree, bool forUpdatePushedDown);
extern Node *build_column_default(Relation rel, int attrno);
--
1.8.3.1
0008-RLS-Enforce-row-security-on-COPY.patchtext/x-patch; name=0008-RLS-Enforce-row-security-on-COPY.patchDownload
>From 4e4fa295080cc497eb4db783b2218050ac68ecf3 Mon Sep 17 00:00:00 2001
From: Craig Ringer <craig@2ndquadrant.com>
Date: Mon, 20 Jan 2014 13:50:23 +0800
Subject: [PATCH 08/10] RLS: Enforce row-security on COPY
---
src/backend/commands/copy.c | 90 ++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 88 insertions(+), 2 deletions(-)
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index f91f164..8e98fd2 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -24,6 +24,7 @@
#include "access/htup_details.h"
#include "access/sysattr.h"
#include "access/xact.h"
+#include "catalog/heap.h"
#include "catalog/namespace.h"
#include "catalog/pg_type.h"
#include "commands/copy.h"
@@ -34,15 +35,19 @@
#include "libpq/pqformat.h"
#include "mb/pg_wchar.h"
#include "miscadmin.h"
+#include "nodes/makefuncs.h"
#include "optimizer/clauses.h"
#include "optimizer/planner.h"
+#include "optimizer/rowsecurity.h"
#include "parser/parse_relation.h"
+#include "parser/parsetree.h"
#include "rewrite/rewriteHandler.h"
#include "storage/fd.h"
#include "tcop/tcopprot.h"
#include "utils/acl.h"
#include "utils/builtins.h"
#include "utils/lsyscache.h"
+#include "utils/syscache.h"
#include "utils/memutils.h"
#include "utils/portal.h"
#include "utils/rel.h"
@@ -814,6 +819,21 @@ DoCopy(const CopyStmt *stmt, const char *queryString, uint64 *processed)
rel = heap_openrv(stmt->relation,
(is_from ? RowExclusiveLock : AccessShareLock));
+ tupDesc = RelationGetDescr(rel);
+ attnums = CopyGetAttnums(tupDesc, rel, stmt->attlist);
+
+ /*
+ * We have to run regular query, if the target relation has
+ * row-level security policy
+ */
+ if (copy_row_security_policy((CopyStmt *)stmt, rel, attnums))
+ {
+ heap_close(rel, NoLock); /* close with keeping lock */
+ relid = InvalidOid;
+ rel = NULL;
+ }
+ else
+ {
relid = RelationGetRelid(rel);
rte = makeNode(RangeTblEntry);
@@ -822,8 +842,6 @@ DoCopy(const CopyStmt *stmt, const char *queryString, uint64 *processed)
rte->relkind = rel->rd_rel->relkind;
rte->requiredPerms = required_access;
- tupDesc = RelationGetDescr(rel);
- attnums = CopyGetAttnums(tupDesc, rel, stmt->attlist);
foreach(cur, attnums)
{
int attno = lfirst_int(cur) -
@@ -835,6 +853,7 @@ DoCopy(const CopyStmt *stmt, const char *queryString, uint64 *processed)
rte->selectedCols = bms_add_member(rte->selectedCols, attno);
}
ExecCheckRTPerms(list_make1(rte), true);
+ }
}
else
{
@@ -1193,6 +1212,53 @@ ProcessCopyOptions(CopyState cstate,
}
/*
+ * Adjust Query tree constructed with row-level security feature.
+ * If WITH OIDS option was supplied, it adds Var node to reference
+ * object-id system column.
+ */
+static void
+fixup_oid_of_rls_query(Query *query)
+{
+ RangeTblEntry *subrte;
+ TargetEntry *subtle;
+ Var *subvar;
+ ListCell *cell;
+ Form_pg_attribute attform
+ = SystemAttributeDefinition(ObjectIdAttributeNumber, true);
+
+ subrte = rt_fetch((Index) 1, query->rtable);
+ Assert(subrte->rtekind == RTE_RELATION);
+
+ if (!SearchSysCacheExists2(ATTNUM,
+ ObjectIdGetDatum(subrte->relid),
+ Int16GetDatum(attform->attnum)))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_COLUMN),
+ errmsg("table \"%s\" does not have OIDs",
+ get_rel_name(subrte->relid))));
+
+ subvar = makeVar((Index) 1,
+ attform->attnum,
+ attform->atttypid,
+ attform->atttypmod,
+ attform->attcollation,
+ 0);
+ subtle = makeTargetEntry((Expr *) subvar,
+ 0,
+ pstrdup(NameStr(attform->attname)),
+ false);
+
+ query->targetList = list_concat(list_make1(subtle),
+ query->targetList);
+ /* adjust resno of TargetEntry */
+ foreach (cell, query->targetList)
+ {
+ subtle = lfirst(cell);
+ subtle->resno++;
+ }
+}
+
+/*
* Common setup routines used by BeginCopyFrom and BeginCopyTo.
*
* Iff <binary>, unload or reload in the binary format, as opposed to the
@@ -1264,6 +1330,25 @@ BeginCopy(bool is_from,
Assert(!is_from);
cstate->rel = NULL;
+ /*
+ * In case when regular COPY TO was replaced because of row-level
+ * security, "raw_query" node have already analyzed / rewritten
+ * query tree.
+ */
+ if (IsA(raw_query, Query))
+ {
+ query = (Query *) raw_query;
+
+ Assert(query->querySource == QSRC_ROW_SECURITY);
+ if (cstate->oids)
+ {
+ fixup_oid_of_rls_query(query);
+ cstate->oids = false;
+ }
+ attnamelist = NIL;
+ }
+ else
+ {
/* Don't allow COPY w/ OIDs from a select */
if (cstate->oids)
ereport(ERROR,
@@ -1288,6 +1373,7 @@ BeginCopy(bool is_from,
elog(ERROR, "unexpected rewrite result");
query = (Query *) linitial(rewritten);
+ }
/* The grammar allows SELECT INTO, but we don't support that */
if (query->utilityStmt != NULL &&
--
1.8.3.1
0009-RLS-Regression-tests.patchtext/x-patch; name=0009-RLS-Regression-tests.patchDownload
>From 6924943ad9b682526de30b39b8b481e6b3c2e3b9 Mon Sep 17 00:00:00 2001
From: Craig Ringer <craig@2ndquadrant.com>
Date: Mon, 20 Jan 2014 12:58:29 +0800
Subject: [PATCH 09/10] RLS: Regression tests
---
src/test/regress/expected/rowsecurity.out | 950 +++++++++++++++++++++++++++++
src/test/regress/expected/sanity_check.out | 1 +
src/test/regress/parallel_schedule | 2 +-
src/test/regress/serial_schedule | 1 +
src/test/regress/sql/rowsecurity.sql | 298 +++++++++
5 files changed, 1251 insertions(+), 1 deletion(-)
create mode 100644 src/test/regress/expected/rowsecurity.out
create mode 100644 src/test/regress/sql/rowsecurity.sql
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
new file mode 100644
index 0000000..04591df
--- /dev/null
+++ b/src/test/regress/expected/rowsecurity.out
@@ -0,0 +1,950 @@
+--
+-- Test of Row-level security feature
+--
+-- Clean up in case a prior regression run failed
+-- Suppress NOTICE messages when users/groups don't exist
+SET client_min_messages TO 'warning';
+DROP USER IF EXISTS rls_regress_user0;
+DROP USER IF EXISTS rls_regress_user1;
+DROP USER IF EXISTS rls_regress_user2;
+DROP SCHEMA IF EXISTS rls_regress_schema CASCADE;
+RESET client_min_messages;
+-- initial setup
+CREATE USER rls_regress_user0;
+CREATE USER rls_regress_user1;
+CREATE USER rls_regress_user2;
+CREATE SCHEMA rls_regress_schema;
+GRANT ALL ON SCHEMA rls_regress_schema TO public;
+SET search_path = rls_regress_schema;
+-- setup of malicious function
+CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool
+ COST 0.0000001 LANGUAGE plpgsql
+ AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END';
+GRANT EXECUTE ON FUNCTION f_leak(text) TO public;
+-- BASIC Row-Level Security Scenario
+SET SESSION AUTHORIZATION rls_regress_user0;
+CREATE TABLE uaccount (
+ pguser name primary key,
+ seclv int
+);
+INSERT INTO uaccount VALUES
+ ('rls_regress_user0', 99),
+ ('rls_regress_user1', 1),
+ ('rls_regress_user2', 2),
+ ('rls_regress_user3', 3);
+GRANT SELECT ON uaccount TO public;
+CREATE TABLE category (
+ cid int primary key,
+ cname text
+);
+GRANT ALL ON category TO public;
+INSERT INTO category VALUES
+ (11, 'novel'),
+ (22, 'science fiction'),
+ (33, 'technology'),
+ (44, 'manga');
+CREATE TABLE document (
+ did int primary key,
+ cid int references category(cid),
+ dlevel int not null,
+ dauthor name,
+ dtitle text
+);
+GRANT ALL ON document TO public;
+INSERT INTO document VALUES
+ ( 1, 11, 1, 'rls_regress_user1', 'my first novel'),
+ ( 2, 11, 2, 'rls_regress_user1', 'my second novel'),
+ ( 3, 22, 2, 'rls_regress_user1', 'my science fiction'),
+ ( 4, 44, 1, 'rls_regress_user1', 'my first manga'),
+ ( 5, 44, 2, 'rls_regress_user1', 'my second manga'),
+ ( 6, 22, 1, 'rls_regress_user2', 'great science fiction'),
+ ( 7, 33, 2, 'rls_regress_user2', 'great technology book'),
+ ( 8, 44, 1, 'rls_regress_user2', 'great manga');
+-- user's security level must higher than or equal to document's one
+ALTER TABLE document SET ROW SECURITY FOR ALL
+ TO (dlevel <= (SELECT seclv FROM uaccount WHERE pguser = current_user));
+-- viewpoint from rls_regress_user1
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE: f_leak => my first novel
+NOTICE: f_leak => my first manga
+NOTICE: f_leak => great science fiction
+NOTICE: f_leak => great manga
+ did | cid | dlevel | dauthor | dtitle
+-----+-----+--------+-------------------+-----------------------
+ 1 | 11 | 1 | rls_regress_user1 | my first novel
+ 4 | 44 | 1 | rls_regress_user1 | my first manga
+ 6 | 22 | 1 | rls_regress_user2 | great science fiction
+ 8 | 44 | 1 | rls_regress_user2 | great manga
+(4 rows)
+
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+NOTICE: f_leak => my first novel
+NOTICE: f_leak => my first manga
+NOTICE: f_leak => great science fiction
+NOTICE: f_leak => great manga
+ cid | did | dlevel | dauthor | dtitle | cname
+-----+-----+--------+-------------------+-----------------------+-----------------
+ 11 | 1 | 1 | rls_regress_user1 | my first novel | novel
+ 22 | 6 | 1 | rls_regress_user2 | great science fiction | science fiction
+ 44 | 8 | 1 | rls_regress_user2 | great manga | manga
+ 44 | 4 | 1 | rls_regress_user1 | my first manga | manga
+(4 rows)
+
+-- viewpoint from rls_regress_user2
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE: f_leak => my first novel
+NOTICE: f_leak => my second novel
+NOTICE: f_leak => my science fiction
+NOTICE: f_leak => my first manga
+NOTICE: f_leak => my second manga
+NOTICE: f_leak => great science fiction
+NOTICE: f_leak => great technology book
+NOTICE: f_leak => great manga
+ did | cid | dlevel | dauthor | dtitle
+-----+-----+--------+-------------------+-----------------------
+ 1 | 11 | 1 | rls_regress_user1 | my first novel
+ 2 | 11 | 2 | rls_regress_user1 | my second novel
+ 3 | 22 | 2 | rls_regress_user1 | my science fiction
+ 4 | 44 | 1 | rls_regress_user1 | my first manga
+ 5 | 44 | 2 | rls_regress_user1 | my second manga
+ 6 | 22 | 1 | rls_regress_user2 | great science fiction
+ 7 | 33 | 2 | rls_regress_user2 | great technology book
+ 8 | 44 | 1 | rls_regress_user2 | great manga
+(8 rows)
+
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+NOTICE: f_leak => my first novel
+NOTICE: f_leak => my second novel
+NOTICE: f_leak => my science fiction
+NOTICE: f_leak => my first manga
+NOTICE: f_leak => my second manga
+NOTICE: f_leak => great science fiction
+NOTICE: f_leak => great technology book
+NOTICE: f_leak => great manga
+ cid | did | dlevel | dauthor | dtitle | cname
+-----+-----+--------+-------------------+-----------------------+-----------------
+ 11 | 2 | 2 | rls_regress_user1 | my second novel | novel
+ 11 | 1 | 1 | rls_regress_user1 | my first novel | novel
+ 22 | 6 | 1 | rls_regress_user2 | great science fiction | science fiction
+ 22 | 3 | 2 | rls_regress_user1 | my science fiction | science fiction
+ 33 | 7 | 2 | rls_regress_user2 | great technology book | technology
+ 44 | 8 | 1 | rls_regress_user2 | great manga | manga
+ 44 | 5 | 2 | rls_regress_user1 | my second manga | manga
+ 44 | 4 | 1 | rls_regress_user1 | my first manga | manga
+(8 rows)
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+ QUERY PLAN
+----------------------------------------------------------
+ Subquery Scan on document
+ Filter: f_leak(document.dtitle)
+ -> Seq Scan on document document_1
+ Filter: (dlevel <= $0)
+ InitPlan 1 (returns $0)
+ -> Index Scan using uaccount_pkey on uaccount
+ Index Cond: (pguser = "current_user"())
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+ QUERY PLAN
+----------------------------------------------------------------------
+ Hash Join
+ Hash Cond: (category.cid = document.cid)
+ -> Seq Scan on category
+ -> Hash
+ -> Subquery Scan on document
+ Filter: f_leak(document.dtitle)
+ -> Seq Scan on document document_1
+ Filter: (dlevel <= $0)
+ InitPlan 1 (returns $0)
+ -> Index Scan using uaccount_pkey on uaccount
+ Index Cond: (pguser = "current_user"())
+(11 rows)
+
+-- only owner can change row-level security
+ALTER TABLE document SET ROW SECURITY FOR ALL TO (true); -- fail
+ERROR: must be owner of relation document
+ALTER TABLE document RESET ROW SECURITY FOR ALL; -- fail
+ERROR: must be owner of relation document
+SET SESSION AUTHORIZATION rls_regress_user0;
+ALTER TABLE document SET ROW SECURITY FOR ALL TO (dauthor = current_user);
+-- viewpoint from rls_regress_user1 again
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE: f_leak => my first novel
+NOTICE: f_leak => my second novel
+NOTICE: f_leak => my science fiction
+NOTICE: f_leak => my first manga
+NOTICE: f_leak => my second manga
+ did | cid | dlevel | dauthor | dtitle
+-----+-----+--------+-------------------+--------------------
+ 1 | 11 | 1 | rls_regress_user1 | my first novel
+ 2 | 11 | 2 | rls_regress_user1 | my second novel
+ 3 | 22 | 2 | rls_regress_user1 | my science fiction
+ 4 | 44 | 1 | rls_regress_user1 | my first manga
+ 5 | 44 | 2 | rls_regress_user1 | my second manga
+(5 rows)
+
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+NOTICE: f_leak => my first novel
+NOTICE: f_leak => my second novel
+NOTICE: f_leak => my science fiction
+NOTICE: f_leak => my first manga
+NOTICE: f_leak => my second manga
+ cid | did | dlevel | dauthor | dtitle | cname
+-----+-----+--------+-------------------+--------------------+-----------------
+ 11 | 1 | 1 | rls_regress_user1 | my first novel | novel
+ 11 | 2 | 2 | rls_regress_user1 | my second novel | novel
+ 22 | 3 | 2 | rls_regress_user1 | my science fiction | science fiction
+ 44 | 4 | 1 | rls_regress_user1 | my first manga | manga
+ 44 | 5 | 2 | rls_regress_user1 | my second manga | manga
+(5 rows)
+
+-- viewpoint from rls_regress_user2 again
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE: f_leak => great science fiction
+NOTICE: f_leak => great technology book
+NOTICE: f_leak => great manga
+ did | cid | dlevel | dauthor | dtitle
+-----+-----+--------+-------------------+-----------------------
+ 6 | 22 | 1 | rls_regress_user2 | great science fiction
+ 7 | 33 | 2 | rls_regress_user2 | great technology book
+ 8 | 44 | 1 | rls_regress_user2 | great manga
+(3 rows)
+
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+NOTICE: f_leak => great science fiction
+NOTICE: f_leak => great technology book
+NOTICE: f_leak => great manga
+ cid | did | dlevel | dauthor | dtitle | cname
+-----+-----+--------+-------------------+-----------------------+-----------------
+ 22 | 6 | 1 | rls_regress_user2 | great science fiction | science fiction
+ 33 | 7 | 2 | rls_regress_user2 | great technology book | technology
+ 44 | 8 | 1 | rls_regress_user2 | great manga | manga
+(3 rows)
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+ QUERY PLAN
+----------------------------------------------
+ Subquery Scan on document
+ Filter: f_leak(document.dtitle)
+ -> Seq Scan on document document_1
+ Filter: (dauthor = "current_user"())
+(4 rows)
+
+EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+ QUERY PLAN
+----------------------------------------------------
+ Nested Loop
+ -> Subquery Scan on document
+ Filter: f_leak(document.dtitle)
+ -> Seq Scan on document document_1
+ Filter: (dauthor = "current_user"())
+ -> Index Scan using category_pkey on category
+ Index Cond: (cid = document.cid)
+(7 rows)
+
+-- interaction of FK/PK constraints
+SET SESSION AUTHORIZATION rls_regress_user0;
+ALTER TABLE category SET ROW SECURITY FOR ALL
+ TO (CASE WHEN current_user = 'rls_regress_user1' THEN cid IN (11, 33)
+ WHEN current_user = 'rls_regress_user2' THEN cid IN (22, 44)
+ ELSE false END);
+-- cannot delete PK referenced by invisible FK
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document d full outer join category c on d.cid = c.cid;
+ did | cid | dlevel | dauthor | dtitle | cid | cname
+-----+-----+--------+-------------------+--------------------+-----+------------
+ 2 | 11 | 2 | rls_regress_user1 | my second novel | 11 | novel
+ 1 | 11 | 1 | rls_regress_user1 | my first novel | 11 | novel
+ | | | | | 33 | technology
+ 5 | 44 | 2 | rls_regress_user1 | my second manga | |
+ 4 | 44 | 1 | rls_regress_user1 | my first manga | |
+ 3 | 22 | 2 | rls_regress_user1 | my science fiction | |
+(6 rows)
+
+DELETE FROM category WHERE cid = 33; -- failed
+ERROR: update or delete on table "category" violates foreign key constraint "document_cid_fkey" on table "document"
+DETAIL: Key (cid)=(33) is still referenced from table "document".
+-- cannot insert FK referencing invisible PK
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document d full outer join category c on d.cid = c.cid;
+ did | cid | dlevel | dauthor | dtitle | cid | cname
+-----+-----+--------+-------------------+-----------------------+-----+-----------------
+ 6 | 22 | 1 | rls_regress_user2 | great science fiction | 22 | science fiction
+ 8 | 44 | 1 | rls_regress_user2 | great manga | 44 | manga
+ 7 | 33 | 2 | rls_regress_user2 | great technology book | |
+(3 rows)
+
+INSERT INTO document VALUES (10, 33, 1, current_user, 'hoge'); -- failed
+ERROR: insert or update on table "document" violates foreign key constraint "document_cid_fkey"
+DETAIL: Key (cid)=(33) is not present in table "category".
+-- database superuser can bypass RLS policy
+RESET SESSION AUTHORIZATION;
+SELECT * FROM document;
+ did | cid | dlevel | dauthor | dtitle
+-----+-----+--------+-------------------+-----------------------
+ 1 | 11 | 1 | rls_regress_user1 | my first novel
+ 2 | 11 | 2 | rls_regress_user1 | my second novel
+ 3 | 22 | 2 | rls_regress_user1 | my science fiction
+ 4 | 44 | 1 | rls_regress_user1 | my first manga
+ 5 | 44 | 2 | rls_regress_user1 | my second manga
+ 6 | 22 | 1 | rls_regress_user2 | great science fiction
+ 7 | 33 | 2 | rls_regress_user2 | great technology book
+ 8 | 44 | 1 | rls_regress_user2 | great manga
+(8 rows)
+
+SELECT * FROM category;
+ cid | cname
+-----+-----------------
+ 11 | novel
+ 22 | science fiction
+ 33 | technology
+ 44 | manga
+(4 rows)
+
+--
+-- Table inheritance and RLS policy
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+CREATE TABLE t1 (a int, junk1 text, b text) WITH OIDS;
+ALTER TABLE t1 DROP COLUMN junk1; -- just a disturbing factor
+GRANT ALL ON t1 TO public;
+COPY t1 FROM stdin WITH (oids);
+CREATE TABLE t2 (c float) INHERITS (t1);
+COPY t2 FROM stdin WITH (oids);
+CREATE TABLE t3 (c text, b text, a int) WITH OIDS;
+ALTER TABLE t3 INHERIT t1;
+COPY t3(a,b,c) FROM stdin WITH (oids);
+ALTER TABLE t1 SET ROW SECURITY FOR ALL TO (a % 2 = 0); -- be even number
+ALTER TABLE t2 SET ROW SECURITY FOR ALL TO (a % 2 = 1); -- be odd number
+SELECT * FROM t1;
+ a | b
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1;
+ QUERY PLAN
+-------------------------------------
+ Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+(8 rows)
+
+SELECT * FROM t1 WHERE f_leak(b);
+NOTICE: f_leak => bbb
+NOTICE: f_leak => ddd
+NOTICE: f_leak => abc
+NOTICE: f_leak => cde
+NOTICE: f_leak => xxx
+NOTICE: f_leak => yyy
+NOTICE: f_leak => zzz
+ a | b
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+ QUERY PLAN
+-------------------------------------
+ Append
+ -> Subquery Scan on t1
+ Filter: f_leak(t1.b)
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ Filter: f_leak(t2.b)
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+ Filter: f_leak(b)
+(11 rows)
+
+-- reference to system column
+SELECT oid, * FROM t1;
+ oid | a | b
+-----+---+-----
+ 102 | 2 | bbb
+ 104 | 4 | ddd
+ 201 | 1 | abc
+ 203 | 3 | cde
+ 301 | 1 | xxx
+ 302 | 2 | yyy
+ 303 | 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1;
+ QUERY PLAN
+-------------------------------------
+ Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+(8 rows)
+
+-- reference to whole-row reference
+SELECT *,t1 FROM t1;
+ a | b | t1
+---+-----+---------
+ 2 | bbb | (2,bbb)
+ 4 | ddd | (4,ddd)
+ 1 | abc | (1,abc)
+ 3 | cde | (3,cde)
+ 1 | xxx | (1,xxx)
+ 2 | yyy | (2,yyy)
+ 3 | zzz | (3,zzz)
+(7 rows)
+
+EXPLAIN (costs off) SELECT *,t1 FROM t1;
+ QUERY PLAN
+-------------------------------------
+ Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+(8 rows)
+
+-- for share/update lock
+SELECT * FROM t1 FOR SHARE;
+ a | b
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 FOR SHARE;
+ QUERY PLAN
+-------------------------------------------
+ LockRows
+ -> Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+(9 rows)
+
+SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+NOTICE: f_leak => bbb
+NOTICE: f_leak => ddd
+NOTICE: f_leak => abc
+NOTICE: f_leak => cde
+NOTICE: f_leak => xxx
+NOTICE: f_leak => yyy
+NOTICE: f_leak => zzz
+ a | b
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+ QUERY PLAN
+-------------------------------------------
+ LockRows
+ -> Append
+ -> Subquery Scan on t1
+ Filter: f_leak(t1.b)
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ Filter: f_leak(t2.b)
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+ Filter: f_leak(b)
+(12 rows)
+
+--
+-- COPY TO statement
+--
+COPY t1 TO stdout;
+2 bbb
+4 ddd
+COPY t1 TO stdout WITH OIDS;
+102 2 bbb
+104 4 ddd
+COPY t2(c,b) TO stdout WITH OIDS;
+201 1.1 abc
+203 3.3 cde
+COPY (SELECT * FROM t1) TO stdout;
+2 bbb
+4 ddd
+1 abc
+3 cde
+1 xxx
+2 yyy
+3 zzz
+COPY document TO stdout WITH OIDS; -- failed (no oid column)
+ERROR: table "document" does not have OIDs
+--
+-- recursive RLS and VIEWs in policy
+--
+CREATE TABLE s1 (a int, b text);
+INSERT INTO s1 (SELECT x, md5(x::text) FROM generate_series(-10,10) x);
+CREATE TABLE s2 (x int, y text);
+INSERT INTO s2 (SELECT x, md5(x::text) FROM generate_series(-6,6) x);
+CREATE VIEW v2 AS SELECT * FROM s2 WHERE y like '%af%';
+ALTER TABLE s1 SET ROW SECURITY FOR ALL
+ TO (a in (select x from s2 where y like '%2f%'));
+ALTER TABLE s2 SET ROW SECURITY FOR ALL
+ TO (x in (select a from s1 where b like '%22%'));
+SELECT * FROM s1 WHERE f_leak(b); -- fail (infinite recursion)
+ERROR: infinite recursion detected for relation "s1"
+ALTER TABLE s2 SET ROW SECURITY FOR ALL TO (x % 2 = 0);
+SELECT * FROM s1 WHERE f_leak(b); -- OK
+NOTICE: f_leak => c81e728d9d4c2f636f067f89cc14862c
+NOTICE: f_leak => a87ff679a2f3e71d9181a67b7542122c
+ a | b
+---+----------------------------------
+ 2 | c81e728d9d4c2f636f067f89cc14862c
+ 4 | a87ff679a2f3e71d9181a67b7542122c
+(2 rows)
+
+EXPLAIN SELECT * FROM only s1 WHERE f_leak(b);
+ QUERY PLAN
+---------------------------------------------------------------------------------------
+ Subquery Scan on s1 (cost=28.55..61.67 rows=205 width=36)
+ Filter: f_leak(s1.b)
+ -> Hash Join (cost=28.55..55.52 rows=615 width=36)
+ Hash Cond: (s1_1.a = s2.x)
+ -> Seq Scan on s1 s1_1 (cost=0.00..22.30 rows=1230 width=36)
+ -> Hash (cost=28.54..28.54 rows=1 width=4)
+ -> HashAggregate (cost=28.53..28.54 rows=1 width=4)
+ -> Subquery Scan on s2 (cost=0.00..28.52 rows=1 width=4)
+ Filter: (s2.y ~~ '%2f%'::text)
+ -> Seq Scan on s2 s2_1 (cost=0.00..28.45 rows=6 width=36)
+ Filter: ((x % 2) = 0)
+(11 rows)
+
+ALTER TABLE s1 SET ROW SECURITY FOR ALL
+ TO (a in (select x from v2)); -- using VIEW in RLS policy
+SELECT * FROM s1 WHERE f_leak(b); -- OK
+NOTICE: f_leak => 0267aaf632e87a63288a08331f22c7c3
+NOTICE: f_leak => 1679091c5a880faf6fb5e6087eb1b2dc
+ a | b
+----+----------------------------------
+ -4 | 0267aaf632e87a63288a08331f22c7c3
+ 6 | 1679091c5a880faf6fb5e6087eb1b2dc
+(2 rows)
+
+EXPLAIN (COSTS OFF) SELECT * FROM s1 WHERE f_leak(b);
+ QUERY PLAN
+----------------------------------------------------------
+ Subquery Scan on s1
+ Filter: f_leak(s1.b)
+ -> Hash Join
+ Hash Cond: (s1_1.a = s2.x)
+ -> Seq Scan on s1 s1_1
+ -> Hash
+ -> HashAggregate
+ -> Subquery Scan on s2
+ Filter: (s2.y ~~ '%af%'::text)
+ -> Seq Scan on s2 s2_1
+ Filter: ((x % 2) = 0)
+(11 rows)
+
+SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+ xx | x | y
+----+----+----------------------------------
+ -6 | -6 | 596a3d04481816330f07e4f97510c28f
+ -4 | -4 | 0267aaf632e87a63288a08331f22c7c3
+ 2 | 2 | c81e728d9d4c2f636f067f89cc14862c
+(3 rows)
+
+EXPLAIN (COSTS OFF) SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+ QUERY PLAN
+--------------------------------------------------------------------
+ Subquery Scan on s2
+ Filter: (s2.y ~~ '%28%'::text)
+ -> Seq Scan on s2 s2_1
+ Filter: ((x % 2) = 0)
+ SubPlan 1
+ -> Limit
+ -> Subquery Scan on s1
+ -> Nested Loop Semi Join
+ Join Filter: (s1_1.a = s2_2.x)
+ -> Seq Scan on s1 s1_1
+ -> Materialize
+ -> Subquery Scan on s2_2
+ Filter: (s2_2.y ~~ '%af%'::text)
+ -> Seq Scan on s2 s2_3
+ Filter: ((x % 2) = 0)
+(15 rows)
+
+ALTER TABLE s2 SET ROW SECURITY FOR ALL
+ TO (x in (select a from s1 where b like '%d2%'));
+SELECT * FROM s1 WHERE f_leak(b); -- fail (infinite recursion via view)
+ERROR: infinite recursion detected for relation "s1"
+-- prepared statement with rls_regress_user0 privilege
+PREPARE p1(int) AS SELECT * FROM t1 WHERE a <= $1;
+EXECUTE p1(2);
+ a | b
+---+-----
+ 2 | bbb
+ 1 | abc
+ 1 | xxx
+ 2 | yyy
+(4 rows)
+
+EXPLAIN (costs off) EXECUTE p1(2);
+ QUERY PLAN
+----------------------------------------------------
+ Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a <= 2) AND ((a % 2) = 0))
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a <= 2) AND ((a % 2) = 1))
+ -> Seq Scan on t3
+ Filter: (a <= 2)
+(9 rows)
+
+-- superuser is allowed to bypass RLS checks
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1 WHERE f_leak(b);
+NOTICE: f_leak => aaa
+NOTICE: f_leak => bbb
+NOTICE: f_leak => ccc
+NOTICE: f_leak => ddd
+NOTICE: f_leak => abc
+NOTICE: f_leak => bcd
+NOTICE: f_leak => cde
+NOTICE: f_leak => def
+NOTICE: f_leak => xxx
+NOTICE: f_leak => yyy
+NOTICE: f_leak => zzz
+ a | b
+---+-----
+ 1 | aaa
+ 2 | bbb
+ 3 | ccc
+ 4 | ddd
+ 1 | abc
+ 2 | bcd
+ 3 | cde
+ 4 | def
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(11 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+ QUERY PLAN
+---------------------------
+ Append
+ -> Seq Scan on t1
+ Filter: f_leak(b)
+ -> Seq Scan on t2
+ Filter: f_leak(b)
+ -> Seq Scan on t3
+ Filter: f_leak(b)
+(7 rows)
+
+-- plan cache should be invalidated
+EXECUTE p1(2);
+ a | b
+---+-----
+ 1 | aaa
+ 2 | bbb
+ 1 | abc
+ 2 | bcd
+ 1 | xxx
+ 2 | yyy
+(6 rows)
+
+EXPLAIN (costs off) EXECUTE p1(2);
+ QUERY PLAN
+--------------------------
+ Append
+ -> Seq Scan on t1
+ Filter: (a <= 2)
+ -> Seq Scan on t2
+ Filter: (a <= 2)
+ -> Seq Scan on t3
+ Filter: (a <= 2)
+(7 rows)
+
+PREPARE p2(int) AS SELECT * FROM t1 WHERE a = $1;
+EXECUTE p2(2);
+ a | b
+---+-----
+ 2 | bbb
+ 2 | bcd
+ 2 | yyy
+(3 rows)
+
+EXPLAIN (costs off) EXECUTE p2(2);
+ QUERY PLAN
+-------------------------
+ Append
+ -> Seq Scan on t1
+ Filter: (a = 2)
+ -> Seq Scan on t2
+ Filter: (a = 2)
+ -> Seq Scan on t3
+ Filter: (a = 2)
+(7 rows)
+
+-- also, case when privilege switch from superuser
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXECUTE p2(2);
+ a | b
+---+-----
+ 2 | bbb
+ 2 | yyy
+(2 rows)
+
+EXPLAIN (costs off) EXECUTE p2(2);
+ QUERY PLAN
+---------------------------------------------------
+ Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a = 2) AND ((a % 2) = 0))
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a = 2) AND ((a % 2) = 1))
+ -> Seq Scan on t3
+ Filter: (a = 2)
+(9 rows)
+
+--
+-- UPDATE / DELETE and Row-level security
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXPLAIN (costs off) UPDATE t1 SET b = b || b WHERE f_leak(b);
+ QUERY PLAN
+-------------------------------------
+ Update on t1
+ -> Subquery Scan on t1_1
+ Filter: f_leak(t1_1.b)
+ -> Seq Scan on t1 t1_2
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ Filter: f_leak(t2.b)
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+ Filter: f_leak(b)
+(11 rows)
+
+UPDATE t1 SET b = b || b WHERE f_leak(b);
+NOTICE: f_leak => bbb
+NOTICE: f_leak => ddd
+NOTICE: f_leak => abc
+NOTICE: f_leak => cde
+NOTICE: f_leak => xxx
+NOTICE: f_leak => yyy
+NOTICE: f_leak => zzz
+EXPLAIN (costs off) UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b);
+ QUERY PLAN
+-------------------------------------
+ Update on t1
+ -> Subquery Scan on t1_1
+ Filter: f_leak(t1_1.b)
+ -> Seq Scan on t1 t1_2
+ Filter: ((a % 2) = 0)
+(5 rows)
+
+UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b);
+NOTICE: f_leak => bbbbbb
+NOTICE: f_leak => dddddd
+-- returning clause with system column
+UPDATE only t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+NOTICE: f_leak => bbbbbb_updt
+NOTICE: f_leak => dddddd_updt
+ oid | a | b | t1
+-----+---+-------------+-----------------
+ 102 | 2 | bbbbbb_updt | (2,bbbbbb_updt)
+ 104 | 4 | dddddd_updt | (4,dddddd_updt)
+(2 rows)
+
+UPDATE t1 SET b = b WHERE f_leak(b) RETURNING *;
+NOTICE: f_leak => bbbbbb_updt
+NOTICE: f_leak => dddddd_updt
+NOTICE: f_leak => abcabc
+NOTICE: f_leak => cdecde
+NOTICE: f_leak => xxxxxx
+NOTICE: f_leak => yyyyyy
+NOTICE: f_leak => zzzzzz
+ a | b
+---+-------------
+ 2 | bbbbbb_updt
+ 4 | dddddd_updt
+ 1 | abcabc
+ 3 | cdecde
+ 1 | xxxxxx
+ 2 | yyyyyy
+ 3 | zzzzzz
+(7 rows)
+
+UPDATE t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+NOTICE: f_leak => bbbbbb_updt
+NOTICE: f_leak => dddddd_updt
+NOTICE: f_leak => abcabc
+NOTICE: f_leak => cdecde
+NOTICE: f_leak => xxxxxx
+NOTICE: f_leak => yyyyyy
+NOTICE: f_leak => zzzzzz
+ oid | a | b | t1
+-----+---+-------------+-----------------
+ 102 | 2 | bbbbbb_updt | (2,bbbbbb_updt)
+ 104 | 4 | dddddd_updt | (4,dddddd_updt)
+ 201 | 1 | abcabc | (1,abcabc)
+ 203 | 3 | cdecde | (3,cdecde)
+ 301 | 1 | xxxxxx | (1,xxxxxx)
+ 302 | 2 | yyyyyy | (2,yyyyyy)
+ 303 | 3 | zzzzzz | (3,zzzzzz)
+(7 rows)
+
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1;
+ a | b
+---+-------------
+ 1 | aaa
+ 3 | ccc
+ 2 | bbbbbb_updt
+ 4 | dddddd_updt
+ 2 | bcd
+ 4 | def
+ 1 | abcabc
+ 3 | cdecde
+ 1 | xxxxxx
+ 2 | yyyyyy
+ 3 | zzzzzz
+(11 rows)
+
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXPLAIN (costs off) DELETE FROM only t1 WHERE f_leak(b);
+ QUERY PLAN
+-------------------------------------
+ Delete on t1
+ -> Subquery Scan on t1_1
+ Filter: f_leak(t1_1.b)
+ -> Seq Scan on t1 t1_2
+ Filter: ((a % 2) = 0)
+(5 rows)
+
+EXPLAIN (costs off) DELETE FROM t1 WHERE f_leak(b);
+ QUERY PLAN
+-------------------------------------
+ Delete on t1
+ -> Subquery Scan on t1_1
+ Filter: f_leak(t1_1.b)
+ -> Seq Scan on t1 t1_2
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ Filter: f_leak(t2.b)
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+ Filter: f_leak(b)
+(11 rows)
+
+DELETE FROM only t1 WHERE f_leak(b) RETURNING oid, *, t1;
+NOTICE: f_leak => bbbbbb_updt
+NOTICE: f_leak => dddddd_updt
+ oid | a | b | t1
+-----+---+-------------+-----------------
+ 102 | 2 | bbbbbb_updt | (2,bbbbbb_updt)
+ 104 | 4 | dddddd_updt | (4,dddddd_updt)
+(2 rows)
+
+DELETE FROM t1 WHERE f_leak(b) RETURNING oid, *, t1;
+NOTICE: f_leak => abcabc
+NOTICE: f_leak => cdecde
+NOTICE: f_leak => xxxxxx
+NOTICE: f_leak => yyyyyy
+NOTICE: f_leak => zzzzzz
+ oid | a | b | t1
+-----+---+--------+------------
+ 201 | 1 | abcabc | (1,abcabc)
+ 203 | 3 | cdecde | (3,cdecde)
+ 301 | 1 | xxxxxx | (1,xxxxxx)
+ 302 | 2 | yyyyyy | (2,yyyyyy)
+ 303 | 3 | zzzzzz | (3,zzzzzz)
+(5 rows)
+
+--
+-- Test psql \dt+ command
+--
+ALTER TABLE category RESET ROW SECURITY FOR ALL; -- too long qual
+\dt+
+ List of relations
+ Schema | Name | Type | Owner | Size | Description | Row-security
+--------------------+----------+-------+-------------------+------------+-------------+----------------------------------
+ rls_regress_schema | category | table | rls_regress_user0 | 16 kB | |
+ rls_regress_schema | document | table | rls_regress_user0 | 16 kB | | (dauthor = "current_user"())
+ rls_regress_schema | s1 | table | rls_regress_user0 | 16 kB | | (a IN ( SELECT v2.x +
+ | | | | | | FROM v2))
+ rls_regress_schema | s2 | table | rls_regress_user0 | 16 kB | | (x IN ( SELECT s1.a +
+ | | | | | | FROM s1 +
+ | | | | | | WHERE (s1.b ~~ '%d2%'::text)))
+ rls_regress_schema | t1 | table | rls_regress_user0 | 16 kB | | ((a % 2) = 0)
+ rls_regress_schema | t2 | table | rls_regress_user0 | 16 kB | | ((a % 2) = 1)
+ rls_regress_schema | t3 | table | rls_regress_user0 | 16 kB | |
+ rls_regress_schema | uaccount | table | rls_regress_user0 | 8192 bytes | |
+(8 rows)
+
+--
+-- Clean up objects
+--
+RESET SESSION AUTHORIZATION;
+DROP SCHEMA rls_regress_schema CASCADE;
+NOTICE: drop cascades to 10 other objects
+DETAIL: drop cascades to function f_leak(text)
+drop cascades to table uaccount
+drop cascades to table category
+drop cascades to table document
+drop cascades to table t1
+drop cascades to table t2
+drop cascades to table t3
+drop cascades to table s1
+drop cascades to table s2
+drop cascades to view v2
+DROP USER rls_regress_user0;
+DROP USER rls_regress_user1;
+DROP USER rls_regress_user2;
diff --git a/src/test/regress/expected/sanity_check.out b/src/test/regress/expected/sanity_check.out
index a62a3e3..a41344b 100644
--- a/src/test/regress/expected/sanity_check.out
+++ b/src/test/regress/expected/sanity_check.out
@@ -121,6 +121,7 @@ pg_pltemplate|t
pg_proc|t
pg_range|t
pg_rewrite|t
+pg_rowsecurity|t
pg_seclabel|t
pg_shdepend|t
pg_shdescription|t
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 5758b07..f7963fc 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 matview lock replica_identity
+test: privileges rowsecurity security_label collate matview lock replica_identity
# ----------
# Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 78348f5..51c4c12 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -94,6 +94,7 @@ test: delete
test: namespace
test: prepared_xacts
test: privileges
+test: rowsecurity
test: security_label
test: collate
test: matview
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
new file mode 100644
index 0000000..55d4aad
--- /dev/null
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -0,0 +1,298 @@
+--
+-- Test of Row-level security feature
+--
+
+-- Clean up in case a prior regression run failed
+
+-- Suppress NOTICE messages when users/groups don't exist
+SET client_min_messages TO 'warning';
+
+DROP USER IF EXISTS rls_regress_user0;
+DROP USER IF EXISTS rls_regress_user1;
+DROP USER IF EXISTS rls_regress_user2;
+
+DROP SCHEMA IF EXISTS rls_regress_schema CASCADE;
+
+RESET client_min_messages;
+
+-- initial setup
+CREATE USER rls_regress_user0;
+CREATE USER rls_regress_user1;
+CREATE USER rls_regress_user2;
+
+CREATE SCHEMA rls_regress_schema;
+GRANT ALL ON SCHEMA rls_regress_schema TO public;
+SET search_path = rls_regress_schema;
+
+-- setup of malicious function
+CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool
+ COST 0.0000001 LANGUAGE plpgsql
+ AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END';
+GRANT EXECUTE ON FUNCTION f_leak(text) TO public;
+
+-- BASIC Row-Level Security Scenario
+
+SET SESSION AUTHORIZATION rls_regress_user0;
+CREATE TABLE uaccount (
+ pguser name primary key,
+ seclv int
+);
+INSERT INTO uaccount VALUES
+ ('rls_regress_user0', 99),
+ ('rls_regress_user1', 1),
+ ('rls_regress_user2', 2),
+ ('rls_regress_user3', 3);
+GRANT SELECT ON uaccount TO public;
+
+CREATE TABLE category (
+ cid int primary key,
+ cname text
+);
+GRANT ALL ON category TO public;
+INSERT INTO category VALUES
+ (11, 'novel'),
+ (22, 'science fiction'),
+ (33, 'technology'),
+ (44, 'manga');
+
+CREATE TABLE document (
+ did int primary key,
+ cid int references category(cid),
+ dlevel int not null,
+ dauthor name,
+ dtitle text
+);
+GRANT ALL ON document TO public;
+INSERT INTO document VALUES
+ ( 1, 11, 1, 'rls_regress_user1', 'my first novel'),
+ ( 2, 11, 2, 'rls_regress_user1', 'my second novel'),
+ ( 3, 22, 2, 'rls_regress_user1', 'my science fiction'),
+ ( 4, 44, 1, 'rls_regress_user1', 'my first manga'),
+ ( 5, 44, 2, 'rls_regress_user1', 'my second manga'),
+ ( 6, 22, 1, 'rls_regress_user2', 'great science fiction'),
+ ( 7, 33, 2, 'rls_regress_user2', 'great technology book'),
+ ( 8, 44, 1, 'rls_regress_user2', 'great manga');
+
+-- user's security level must higher than or equal to document's one
+ALTER TABLE document SET ROW SECURITY FOR ALL
+ TO (dlevel <= (SELECT seclv FROM uaccount WHERE pguser = current_user));
+
+-- viewpoint from rls_regress_user1
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+-- viewpoint from rls_regress_user2
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+-- only owner can change row-level security
+ALTER TABLE document SET ROW SECURITY FOR ALL TO (true); -- fail
+ALTER TABLE document RESET ROW SECURITY FOR ALL; -- fail
+
+SET SESSION AUTHORIZATION rls_regress_user0;
+ALTER TABLE document SET ROW SECURITY FOR ALL TO (dauthor = current_user);
+
+-- viewpoint from rls_regress_user1 again
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+-- viewpoint from rls_regress_user2 again
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+-- interaction of FK/PK constraints
+SET SESSION AUTHORIZATION rls_regress_user0;
+ALTER TABLE category SET ROW SECURITY FOR ALL
+ TO (CASE WHEN current_user = 'rls_regress_user1' THEN cid IN (11, 33)
+ WHEN current_user = 'rls_regress_user2' THEN cid IN (22, 44)
+ ELSE false END);
+
+-- cannot delete PK referenced by invisible FK
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document d full outer join category c on d.cid = c.cid;
+DELETE FROM category WHERE cid = 33; -- failed
+
+-- cannot insert FK referencing invisible PK
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document d full outer join category c on d.cid = c.cid;
+INSERT INTO document VALUES (10, 33, 1, current_user, 'hoge'); -- failed
+
+-- database superuser can bypass RLS policy
+RESET SESSION AUTHORIZATION;
+SELECT * FROM document;
+SELECT * FROM category;
+
+--
+-- Table inheritance and RLS policy
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+
+CREATE TABLE t1 (a int, junk1 text, b text) WITH OIDS;
+ALTER TABLE t1 DROP COLUMN junk1; -- just a disturbing factor
+GRANT ALL ON t1 TO public;
+
+COPY t1 FROM stdin WITH (oids);
+101 1 aaa
+102 2 bbb
+103 3 ccc
+104 4 ddd
+\.
+
+CREATE TABLE t2 (c float) INHERITS (t1);
+COPY t2 FROM stdin WITH (oids);
+201 1 abc 1.1
+202 2 bcd 2.2
+203 3 cde 3.3
+204 4 def 4.4
+\.
+
+CREATE TABLE t3 (c text, b text, a int) WITH OIDS;
+ALTER TABLE t3 INHERIT t1;
+COPY t3(a,b,c) FROM stdin WITH (oids);
+301 1 xxx X
+302 2 yyy Y
+303 3 zzz Z
+\.
+
+ALTER TABLE t1 SET ROW SECURITY FOR ALL TO (a % 2 = 0); -- be even number
+ALTER TABLE t2 SET ROW SECURITY FOR ALL TO (a % 2 = 1); -- be odd number
+
+SELECT * FROM t1;
+EXPLAIN (costs off) SELECT * FROM t1;
+
+SELECT * FROM t1 WHERE f_leak(b);
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+
+-- reference to system column
+SELECT oid, * FROM t1;
+EXPLAIN (costs off) SELECT * FROM t1;
+
+-- reference to whole-row reference
+SELECT *,t1 FROM t1;
+EXPLAIN (costs off) SELECT *,t1 FROM t1;
+
+-- for share/update lock
+SELECT * FROM t1 FOR SHARE;
+EXPLAIN (costs off) SELECT * FROM t1 FOR SHARE;
+
+SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+
+--
+-- COPY TO statement
+--
+COPY t1 TO stdout;
+COPY t1 TO stdout WITH OIDS;
+COPY t2(c,b) TO stdout WITH OIDS;
+COPY (SELECT * FROM t1) TO stdout;
+COPY document TO stdout WITH OIDS; -- failed (no oid column)
+
+--
+-- recursive RLS and VIEWs in policy
+--
+CREATE TABLE s1 (a int, b text);
+INSERT INTO s1 (SELECT x, md5(x::text) FROM generate_series(-10,10) x);
+
+CREATE TABLE s2 (x int, y text);
+INSERT INTO s2 (SELECT x, md5(x::text) FROM generate_series(-6,6) x);
+CREATE VIEW v2 AS SELECT * FROM s2 WHERE y like '%af%';
+
+ALTER TABLE s1 SET ROW SECURITY FOR ALL
+ TO (a in (select x from s2 where y like '%2f%'));
+
+ALTER TABLE s2 SET ROW SECURITY FOR ALL
+ TO (x in (select a from s1 where b like '%22%'));
+
+SELECT * FROM s1 WHERE f_leak(b); -- fail (infinite recursion)
+
+ALTER TABLE s2 SET ROW SECURITY FOR ALL TO (x % 2 = 0);
+
+SELECT * FROM s1 WHERE f_leak(b); -- OK
+EXPLAIN SELECT * FROM only s1 WHERE f_leak(b);
+
+ALTER TABLE s1 SET ROW SECURITY FOR ALL
+ TO (a in (select x from v2)); -- using VIEW in RLS policy
+SELECT * FROM s1 WHERE f_leak(b); -- OK
+EXPLAIN (COSTS OFF) SELECT * FROM s1 WHERE f_leak(b);
+
+SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+EXPLAIN (COSTS OFF) SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+
+ALTER TABLE s2 SET ROW SECURITY FOR ALL
+ TO (x in (select a from s1 where b like '%d2%'));
+SELECT * FROM s1 WHERE f_leak(b); -- fail (infinite recursion via view)
+
+-- prepared statement with rls_regress_user0 privilege
+PREPARE p1(int) AS SELECT * FROM t1 WHERE a <= $1;
+EXECUTE p1(2);
+EXPLAIN (costs off) EXECUTE p1(2);
+
+-- superuser is allowed to bypass RLS checks
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1 WHERE f_leak(b);
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+
+-- plan cache should be invalidated
+EXECUTE p1(2);
+EXPLAIN (costs off) EXECUTE p1(2);
+
+PREPARE p2(int) AS SELECT * FROM t1 WHERE a = $1;
+EXECUTE p2(2);
+EXPLAIN (costs off) EXECUTE p2(2);
+
+-- also, case when privilege switch from superuser
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXECUTE p2(2);
+EXPLAIN (costs off) EXECUTE p2(2);
+
+--
+-- UPDATE / DELETE and Row-level security
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXPLAIN (costs off) UPDATE t1 SET b = b || b WHERE f_leak(b);
+UPDATE t1 SET b = b || b WHERE f_leak(b);
+
+EXPLAIN (costs off) UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b);
+UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b);
+
+-- returning clause with system column
+UPDATE only t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+UPDATE t1 SET b = b WHERE f_leak(b) RETURNING *;
+UPDATE t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1;
+
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXPLAIN (costs off) DELETE FROM only t1 WHERE f_leak(b);
+EXPLAIN (costs off) DELETE FROM t1 WHERE f_leak(b);
+
+DELETE FROM only t1 WHERE f_leak(b) RETURNING oid, *, t1;
+DELETE FROM t1 WHERE f_leak(b) RETURNING oid, *, t1;
+
+--
+-- Test psql \dt+ command
+--
+ALTER TABLE category RESET ROW SECURITY FOR ALL; -- too long qual
+\dt+
+
+--
+-- Clean up objects
+--
+RESET SESSION AUTHORIZATION;
+
+DROP SCHEMA rls_regress_schema CASCADE;
+
+DROP USER rls_regress_user0;
+DROP USER rls_regress_user1;
+DROP USER rls_regress_user2;
--
1.8.3.1
0010-RLS-Supplimental-docs.patchtext/x-patch; name=0010-RLS-Supplimental-docs.patchDownload
>From 63e90965e8ce0d89113002c6df283761ffc6adfc Mon Sep 17 00:00:00 2001
From: Craig Ringer <craig@2ndquadrant.com>
Date: Mon, 20 Jan 2014 13:50:37 +0800
Subject: [PATCH 10/10] RLS: Supplimental docs
---
doc/src/sgml/user-manag.sgml | 145 +++++++++++++++++++++++++++++++++++++++++++
1 file changed, 145 insertions(+)
diff --git a/doc/src/sgml/user-manag.sgml b/doc/src/sgml/user-manag.sgml
index 177ac7a..c764189 100644
--- a/doc/src/sgml/user-manag.sgml
+++ b/doc/src/sgml/user-manag.sgml
@@ -439,4 +439,149 @@ DROP ROLE <replaceable>name</replaceable>;
</para>
</sect1>
+ <sect1 id="row-security">
+ <title>Row-security</title>
+ <para>
+ <productname>PostgreSQL</productname> v9.4 or later provides
+ row-security feature, like several commercial database
+ management system. It allows table owner to assign a particular
+ condition that performs as a security policy of the table; only
+ rows that satisfies the condition should be visible, except for
+ a case when superuser runs queries.
+ </para>
+ <para>
+ Row-security policy can be set using <literal>SET ROW SECURITY</literal>
+ command of <xref linkend="SQL-ALTERTABLE"> 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.
+ </para>
+ <para>
+ For example, the following <literal>customer</literal> table has
+ <literal>uname</literal> field to store user name, and it assume
+ we don't want to expose any properties of other customers.
+ The following command set <literal>current_user = uname</literal>
+ as row-security policy on the <literal>customer</literal> table.
+<screen>
+postgres=> ALTER TABLE customer SET ROW SECURITY
+ FOR ALL TO (current_user = uname);
+ALTER TABLE
+</screen>
+ <xref linkend="SQL-EXPLAIN"> command shows how row-security policy
+ works on the supplied query.
+<screen>
+postgres=> EXPLAIN(costs off) SELECT * FROM customer WHERE f_leak(upasswd);
+ QUERY PLAN
+--------------------------------------------
+ Subquery Scan on customer
+ Filter: f_leak(customer.upasswd)
+ -> Seq Scan on customer customer_1
+ Filter: ("current_user"() = uname)
+(4 rows)
+</screen>
+ This query execution plan means the preconfigured row-security policy is
+ implicitly added, and scan plan on the target relation being wrapped up
+ with a sub-query.
+ It ensures user given qualifiers, including functions with side effects,
+ are never executed earlier than the row-security policy regardless of
+ its cost, except for the cases when these were fully leakproof.
+ This design helps to tackle the scenario described in
+ <xref linkend="RULES-PRIVILEGES">; that introduces the order to evaluate
+ qualifiers is significant to keep confidentiality of invisible rows.
+ </para>
+
+ <para>
+ On the other hand, this design allows superusers to bypass checks with
+ row-security.
+ It ensures <application>pg_dump</application> can obtain a complete set
+ of database backup, and avoid to execute Trojan horse trap, being injected
+ as a row-security policy of user-defined table, with privileges of
+ superuser.
+ </para>
+
+ <para>
+ In case of queries on inherited tables, row-security policy of the parent
+ relation is not applied to child relations. Scope of the row-security
+ policy is limited to the relation on which it is set.
+<screen>
+postgres=> EXPLAIN(costs off) SELECT * FROM t1 WHERE f_leak(y);
+ QUERY PLAN
+-------------------------------------
+ Append
+ -> Subquery Scan on t1
+ Filter: f_leak(t1.y)
+ -> Seq Scan on t1 t1_1
+ Filter: ((x % 2) = 0)
+ -> Seq Scan on t2
+ Filter: f_leak(y)
+ -> Subquery Scan on t3
+ Filter: f_leak(t3.y)
+ -> Seq Scan on t3 t3_1
+ Filter: ((x % 2) = 1)
+(11 rows)
+</screen>
+ In the above example, <literal>t1</literal> has inherited
+ child table <literal>t2</literal> and <literal>t3</literal>,
+ and row-security policy is set on only <literal>t1</literal>,
+ and <literal>t3</literal>, not <literal>t2</literal>.
+
+ The row-security policy of <literal>t1</literal>, <literal>x</literal>
+ must be even-number, is appended only <literal>t1</literal>,
+ neither <literal>t2</literal> nor <literal>t3</literal>.
+ On the contrary, <literal>t3</literal> has different row-security policy;
+ <literal>x</literal> must be odd-number.
+ </para>
+
+ <para>
+ Row-security feature also works to queries for writer-operations; such as
+ <xref linkend="SQL-INSERT">, <xref linkend="SQL-UPDATE"> or
+ <xref linkend="SQL-DELETE"> commands.
+ It ensures all the modified rows satisfies configured row-security policy.
+ The below query tries to update e-mail address of the
+ <literal>customer</> table, and configured row-security makes sure all the
+ modified rows's <literal>uname</> field has to match with
+ <literal>current_user</>.
+ </para>
+<screen>
+postgres=> EXPLAIN (costs off) UPDATE customer SET email = 'alice@example.com';
+ QUERY PLAN
+--------------------------------------------------
+ Update on customer
+ -> Subquery Scan on customer
+ -> Seq Scan on customer customer_1
+ Filter: ("current_user"() = uname)
+(4 rows)
+</screen>
+ <para>
+ Modification of a certain table is consist of two different stuffs; one
+ is fetch rows to be modified from the result relation (except for
+ <literal>INSERT</> command), second is insertion of rows to the
+ result relation. Usually, before-row trigger or check constraints are
+ run in the second phase. In addition, row-security policy is also checked
+ on this stage, to prevent to insert or update rows with unprivileged
+ values.
+ </para>
+
+ <para>
+ Unlike other commercial database systems, we don't have any plan to allow
+ individual row-security policy for each command type. Even if we want to
+ perform with difference policy between <xref linkend="SQL-SELECT"> and
+ <xref linkend="SQL-DELETE">, <literal>RETURNING</> clause can leak the rows
+ to be invisible using <xref linkend="SQL-DELETE"> command.
+ </para>
+
+ <para>
+ Even though it is not a specific matter in row-security, please be careful
+ if you plan to use <literal>current_user</> in row-level security policy.
+ <xref linkend="SQL-DECLARE"> and <xref linkend="SQL-FETCH"> allows to
+ switch current user identifier during execution of the query.
+ Thus, it can fetch the first 100 rows with privilege of <literal>alice</>,
+ then remaining rows with privilege of <literal>bob</>. If and when query
+ execution plan contains some kind of materialization and row-security
+ policy contains <literal>current_user</>, the fetched tuples in
+ <literal>bob</>'s screen might be evaluated according to the privilege of
+ <literal>alice</>.
+ </para>
+ </sect1>
</chapter>
--
1.8.3.1
(Re-sending; I forgot to cc the list)
On 01/20/2014 02:15 PM, Craig Ringer wrote:
On 01/20/2014 09:58 AM, Craig Ringer wrote:
As it is I'm spending today reworking the RLS patch on top of the new
approach to updatable security barrier views.To get that rolling I've split the RLS patch up into chunks, so we can
argue about the catalogs, ALTER syntax, and the actual row-filtering
implementation separately ;-)It's currently on git@github.com:ringerc/postgres.git in the branch
rls-9.4-split, which is subject to rebasing. I'm still going through it
making sure each chunk at least compiles and preferably passes "make
check".
That branch is now pretty stable, and passes checks at every stage up to
the new RLS regression tests. I've pushed a new version to branch
rls-9.4-split. Further updates will rebase this branch.
The tag rls-9.4-split-v5 identifies this particular push, and won't get
rebased away.
I found a number of merge errors and problems that I've corrected and
incorporated into the history, including a bad pg_class.h edit, a
missing break; in tablecmds.c, a missing Node* assignment in the parser
grammar for RESET ROW SECURITY, etc.
A number of concerns remain outstanding, including those Greg listed
except for the regression tests, which I've fixed, and rebasing on top
of current master.
-Continue to expand the functional tests
-Is there enough information about row security available in psql
output? For example, there's nothing in "\d" output that suggests it
might exist. pg_rowsecurity is a monster to try and read.
-Documentation needs plenty of editing, which I can take care of.
Additionally, the issue I found earlier remains outstanding:
- Fails to deal with user-id change during portal execution in (eg)
SECURITY DEFINER funcs returning refcursor, or SQL cursors open
across SET SESSION AUTHORIZATION. `current_user` returns value
after change, not original value, causing issues with RLS predicates.
Now that the patch is split up into stages I'm going to work on
replacing patches 7 and 8 (query plan transformation, COPY support) with
an approach based on Dean's updaable security barriers code.
If anyone else picks up work on this, **please** try to keep this patch
series intact, or even better, move the regression tests back into the
relevant patches as you go. If you send an updated version of the RLS
patch please send a git ref too; that'll make it much easier to pull
your changes and integrate them into the relevant patch in the series
with appropriate "git rebase --interactive" (ab)use.
--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
Attachments:
0010-RLS-Supplimental-docs.patchtext/x-patch; name=0010-RLS-Supplimental-docs.patchDownload
>From bc5a54a13efff6b8deaa433f51578410681fed9d Mon Sep 17 00:00:00 2001
From: Craig Ringer <craig@2ndquadrant.com>
Date: Mon, 20 Jan 2014 13:50:37 +0800
Subject: [PATCH 10/10] RLS: Supplimental docs
---
doc/src/sgml/user-manag.sgml | 145 +++++++++++++++++++++++++++++++++++++++++++
1 file changed, 145 insertions(+)
diff --git a/doc/src/sgml/user-manag.sgml b/doc/src/sgml/user-manag.sgml
index 177ac7a..c764189 100644
--- a/doc/src/sgml/user-manag.sgml
+++ b/doc/src/sgml/user-manag.sgml
@@ -439,4 +439,149 @@ DROP ROLE <replaceable>name</replaceable>;
</para>
</sect1>
+ <sect1 id="row-security">
+ <title>Row-security</title>
+ <para>
+ <productname>PostgreSQL</productname> v9.4 or later provides
+ row-security feature, like several commercial database
+ management system. It allows table owner to assign a particular
+ condition that performs as a security policy of the table; only
+ rows that satisfies the condition should be visible, except for
+ a case when superuser runs queries.
+ </para>
+ <para>
+ Row-security policy can be set using <literal>SET ROW SECURITY</literal>
+ command of <xref linkend="SQL-ALTERTABLE"> 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.
+ </para>
+ <para>
+ For example, the following <literal>customer</literal> table has
+ <literal>uname</literal> field to store user name, and it assume
+ we don't want to expose any properties of other customers.
+ The following command set <literal>current_user = uname</literal>
+ as row-security policy on the <literal>customer</literal> table.
+<screen>
+postgres=> ALTER TABLE customer SET ROW SECURITY
+ FOR ALL TO (current_user = uname);
+ALTER TABLE
+</screen>
+ <xref linkend="SQL-EXPLAIN"> command shows how row-security policy
+ works on the supplied query.
+<screen>
+postgres=> EXPLAIN(costs off) SELECT * FROM customer WHERE f_leak(upasswd);
+ QUERY PLAN
+--------------------------------------------
+ Subquery Scan on customer
+ Filter: f_leak(customer.upasswd)
+ -> Seq Scan on customer customer_1
+ Filter: ("current_user"() = uname)
+(4 rows)
+</screen>
+ This query execution plan means the preconfigured row-security policy is
+ implicitly added, and scan plan on the target relation being wrapped up
+ with a sub-query.
+ It ensures user given qualifiers, including functions with side effects,
+ are never executed earlier than the row-security policy regardless of
+ its cost, except for the cases when these were fully leakproof.
+ This design helps to tackle the scenario described in
+ <xref linkend="RULES-PRIVILEGES">; that introduces the order to evaluate
+ qualifiers is significant to keep confidentiality of invisible rows.
+ </para>
+
+ <para>
+ On the other hand, this design allows superusers to bypass checks with
+ row-security.
+ It ensures <application>pg_dump</application> can obtain a complete set
+ of database backup, and avoid to execute Trojan horse trap, being injected
+ as a row-security policy of user-defined table, with privileges of
+ superuser.
+ </para>
+
+ <para>
+ In case of queries on inherited tables, row-security policy of the parent
+ relation is not applied to child relations. Scope of the row-security
+ policy is limited to the relation on which it is set.
+<screen>
+postgres=> EXPLAIN(costs off) SELECT * FROM t1 WHERE f_leak(y);
+ QUERY PLAN
+-------------------------------------
+ Append
+ -> Subquery Scan on t1
+ Filter: f_leak(t1.y)
+ -> Seq Scan on t1 t1_1
+ Filter: ((x % 2) = 0)
+ -> Seq Scan on t2
+ Filter: f_leak(y)
+ -> Subquery Scan on t3
+ Filter: f_leak(t3.y)
+ -> Seq Scan on t3 t3_1
+ Filter: ((x % 2) = 1)
+(11 rows)
+</screen>
+ In the above example, <literal>t1</literal> has inherited
+ child table <literal>t2</literal> and <literal>t3</literal>,
+ and row-security policy is set on only <literal>t1</literal>,
+ and <literal>t3</literal>, not <literal>t2</literal>.
+
+ The row-security policy of <literal>t1</literal>, <literal>x</literal>
+ must be even-number, is appended only <literal>t1</literal>,
+ neither <literal>t2</literal> nor <literal>t3</literal>.
+ On the contrary, <literal>t3</literal> has different row-security policy;
+ <literal>x</literal> must be odd-number.
+ </para>
+
+ <para>
+ Row-security feature also works to queries for writer-operations; such as
+ <xref linkend="SQL-INSERT">, <xref linkend="SQL-UPDATE"> or
+ <xref linkend="SQL-DELETE"> commands.
+ It ensures all the modified rows satisfies configured row-security policy.
+ The below query tries to update e-mail address of the
+ <literal>customer</> table, and configured row-security makes sure all the
+ modified rows's <literal>uname</> field has to match with
+ <literal>current_user</>.
+ </para>
+<screen>
+postgres=> EXPLAIN (costs off) UPDATE customer SET email = 'alice@example.com';
+ QUERY PLAN
+--------------------------------------------------
+ Update on customer
+ -> Subquery Scan on customer
+ -> Seq Scan on customer customer_1
+ Filter: ("current_user"() = uname)
+(4 rows)
+</screen>
+ <para>
+ Modification of a certain table is consist of two different stuffs; one
+ is fetch rows to be modified from the result relation (except for
+ <literal>INSERT</> command), second is insertion of rows to the
+ result relation. Usually, before-row trigger or check constraints are
+ run in the second phase. In addition, row-security policy is also checked
+ on this stage, to prevent to insert or update rows with unprivileged
+ values.
+ </para>
+
+ <para>
+ Unlike other commercial database systems, we don't have any plan to allow
+ individual row-security policy for each command type. Even if we want to
+ perform with difference policy between <xref linkend="SQL-SELECT"> and
+ <xref linkend="SQL-DELETE">, <literal>RETURNING</> clause can leak the rows
+ to be invisible using <xref linkend="SQL-DELETE"> command.
+ </para>
+
+ <para>
+ Even though it is not a specific matter in row-security, please be careful
+ if you plan to use <literal>current_user</> in row-level security policy.
+ <xref linkend="SQL-DECLARE"> and <xref linkend="SQL-FETCH"> allows to
+ switch current user identifier during execution of the query.
+ Thus, it can fetch the first 100 rows with privilege of <literal>alice</>,
+ then remaining rows with privilege of <literal>bob</>. If and when query
+ execution plan contains some kind of materialization and row-security
+ policy contains <literal>current_user</>, the fetched tuples in
+ <literal>bob</>'s screen might be evaluated according to the privilege of
+ <literal>alice</>.
+ </para>
+ </sect1>
</chapter>
--
1.8.3.1
0001-RLS-Make-plan-caching-dependent-on-user-ID.patchtext/x-patch; name=0001-RLS-Make-plan-caching-dependent-on-user-ID.patchDownload
>From 5d8145dd0c47fb891139568f82ac87bc2403cfec Mon Sep 17 00:00:00 2001
From: Craig Ringer <craig@2ndquadrant.com>
Date: Mon, 20 Jan 2014 13:44:45 +0800
Subject: [PATCH 01/10] RLS: Make plan caching dependent on user ID.
---
src/backend/optimizer/plan/planner.c | 2 ++
src/backend/utils/cache/plancache.c | 32 ++++++++++++++++++++++++++++++++
src/include/nodes/plannodes.h | 2 ++
src/include/nodes/relation.h | 2 ++
src/include/utils/plancache.h | 2 ++
5 files changed, 40 insertions(+)
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 35bda67..8c12eb9 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -177,6 +177,7 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams)
glob->lastPHId = 0;
glob->lastRowMarkId = 0;
glob->transientPlan = false;
+ glob->planUserId = InvalidOid;
/* Determine what fraction of the plan is likely to be scanned */
if (cursorOptions & CURSOR_OPT_FAST_PLAN)
@@ -254,6 +255,7 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams)
result->relationOids = glob->relationOids;
result->invalItems = glob->invalItems;
result->nParamExec = glob->nParamExec;
+ result->planUserId = glob->planUserId;
return result;
}
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index d492cbb..77de2df 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -53,6 +53,7 @@
#include "catalog/namespace.h"
#include "executor/executor.h"
#include "executor/spi.h"
+#include "miscadmin.h"
#include "nodes/nodeFuncs.h"
#include "optimizer/cost.h"
#include "optimizer/planmain.h"
@@ -795,6 +796,16 @@ CheckCachedPlan(CachedPlanSource *plansource)
AcquireExecutorLocks(plan->stmt_list, true);
/*
+ * If plan was constructed with assumption of a particular user-id,
+ * and it is different from the current one, the cached-plan shall
+ * be invalidated to construct suitable query plan.
+ */
+ if (plan->is_valid &&
+ OidIsValid(plan->planUserId) &&
+ plan->planUserId == GetUserId())
+ plan->is_valid = false;
+
+ /*
* If plan was transient, check to see if TransactionXmin has
* advanced, and if so invalidate it.
*/
@@ -847,6 +858,8 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
{
CachedPlan *plan;
List *plist;
+ ListCell *cell;
+ Oid planUserId = InvalidOid;
bool snapshot_set;
bool spi_pushed;
MemoryContext plan_context;
@@ -914,6 +927,24 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
PopActiveSnapshot();
/*
+ * Check whether the generated plan assumes a particular user-id, or not.
+ * In case when a valid user-id is recorded on PlannedStmt->planUserId,
+ * it should be kept and used to validation check of the cached plan
+ * under the "current" user-id.
+ */
+ foreach (cell, plist)
+ {
+ PlannedStmt *pstmt = lfirst(cell);
+
+ if (IsA(pstmt, PlannedStmt) && OidIsValid(pstmt->planUserId))
+ {
+ Assert(!OidIsValid(planUserId) || planUserId == pstmt->planUserId);
+
+ planUserId = pstmt->planUserId;
+ }
+ }
+
+ /*
* Normally we make a dedicated memory context for the CachedPlan and its
* subsidiary data. (It's probably not going to be large, but just in
* case, use the default maxsize parameter. It's transient for the
@@ -956,6 +987,7 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
plan->is_oneshot = plansource->is_oneshot;
plan->is_saved = false;
plan->is_valid = true;
+ plan->planUserId = planUserId;
/* assign generation number to new plan */
plan->generation = ++(plansource->generation);
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 38c039c..35c88cc 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -67,6 +67,8 @@ typedef struct PlannedStmt
List *invalItems; /* other dependencies, as PlanInvalItems */
int nParamExec; /* number of PARAM_EXEC Params used */
+
+ Oid planUserId; /* user-id this plan assumed, or InvalidOid */
} PlannedStmt;
/* macro for fetching the Plan associated with a SubPlan node */
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 8aa40d0..74d0e12 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -99,6 +99,8 @@ typedef struct PlannerGlobal
Index lastRowMarkId; /* highest PlanRowMark ID assigned */
bool transientPlan; /* redo plan when TransactionXmin changes? */
+
+ Oid planUserId; /* User-Id to be assumed on this plan */
} PlannerGlobal;
/* macro for fetching the Plan associated with a SubPlan node */
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index b8ca643..ff89700 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -128,6 +128,8 @@ typedef struct CachedPlan
bool is_oneshot; /* is it a "oneshot" plan? */
bool is_saved; /* is CachedPlan in a long-lived context? */
bool is_valid; /* is the stmt_list currently valid? */
+ Oid planUserId; /* is user-id that is assumed on this cached
+ plan, or InvalidOid if portable for anybody */
TransactionId saved_xmin; /* if valid, replan when TransactionXmin
* changes from this value */
int generation; /* parent's generation number for this plan */
--
1.8.3.1
0002-RLS-add-pg_rowsecurity-catalog.patchtext/x-patch; name=0002-RLS-add-pg_rowsecurity-catalog.patchDownload
>From ad60419074deef9f3d057e395ff08375afd9eeaa Mon Sep 17 00:00:00 2001
From: Craig Ringer <craig@2ndquadrant.com>
Date: Mon, 20 Jan 2014 13:07:50 +0800
Subject: [PATCH 02/10] RLS: add pg_rowsecurity catalog
---
doc/src/sgml/catalogs.sgml | 65 +++++++
src/backend/catalog/Makefile | 4 +-
src/backend/catalog/dependency.c | 8 +
src/backend/catalog/heap.c | 1 +
src/backend/catalog/objectaddress.c | 50 +++++
src/backend/catalog/pg_rowsecurity.c | 287 +++++++++++++++++++++++++++++
src/backend/utils/cache/relcache.c | 17 +-
src/include/catalog/dependency.h | 1 +
src/include/catalog/indexing.h | 5 +
src/include/catalog/pg_class.h | 25 +--
src/include/catalog/pg_rowsecurity.h | 74 ++++++++
src/include/parser/parse_node.h | 3 +-
src/include/utils/rel.h | 2 +
src/test/regress/expected/sanity_check.out | 1 +
14 files changed, 528 insertions(+), 15 deletions(-)
create mode 100644 src/backend/catalog/pg_rowsecurity.c
create mode 100644 src/include/catalog/pg_rowsecurity.h
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 2230c93..1ca088a 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -234,6 +234,11 @@
</row>
<row>
+ <entry><link linkend="catalog-pg-rowsecurity"><structname>pg_rowsecuritylevelsec</structname></link></entry>
+ <entry>row-level security policy of relation</entry>
+ </row>
+
+ <row>
<entry><link linkend="catalog-pg-seclabel"><structname>pg_seclabel</structname></link></entry>
<entry>security labels on database objects</entry>
</row>
@@ -1876,6 +1881,16 @@
</row>
<row>
+ <entry><structfield>relhasrowsecurity</structfield></entry>
+ <entry><type>bool</type></entry>
+ <entry></entry>
+ <entry>
+ True if table has row-security policy; see
+ <link linkend="catalog-pg-rowsecurity"><structname>pg_rowsecurity</structname></link> catalog
+ </entry>
+ </row>
+
+ <row>
<entry><structfield>relhassubclass</structfield></entry>
<entry><type>bool</type></entry>
<entry></entry>
@@ -5157,6 +5172,56 @@
</sect1>
+ <sect1 id="catalog-pg-rowsecurity">
+ <title><structname>pg_rowsecurity</structname></title>
+
+ <indexterm zone="catalog-pg-rowsecurity">
+ <primary>pg_rowsecurity</primary>
+ </indexterm>
+ <para>
+ The catalog <structname>pg_rowsecurity</structname> stores expression
+ tree of row-security policy to be performed on a particular relation.
+ </para>
+ <table>
+ <title><structname>pg_rowsecurity</structname> Columns</title>
+ <tgroup cols="4">
+ <thead>
+ <row>
+ <entry>Name</entry>
+ <entry>Type</entry>
+ <entry>References</entry>
+ <entry>Description</entry>
+ </row>
+ </thead>
+ <tbody>
+ <row>
+ <entry><structfield>rsecrelid</structfield></entry>
+ <entry><type>oid</type></entry>
+ <entry><literal><link linkend="catalog-pg-class"><structname>pg_class</structname></link>.oid</literal></entry>
+ <entry>The table this row-security is for</entry>
+ </row>
+ <row>
+ <entry><structfield>rseccmd</structfield></entry>
+ <entry><type>char</type></entry>
+ <entry></entry>
+ <entry>The command this row-security is for. 'a' meaning all is only possible value right now.</entry>
+ </row>
+ <row>
+ <entry><structfield>rsecqual</structfield></entry>
+ <entry><type>pg_node_tree</type></entry>
+ <entry></entry>
+ <entry>An expression tree to be performed as row-security policy</entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+ <note>
+ <para>
+ <literal>pg_class.relhasrowsecurity</literal>
+ must be true if a table has row-security policy in this catalog.
+ </para>
+ </note>
+ </sect1>
<sect1 id="catalog-pg-seclabel">
<title><structname>pg_seclabel</structname></title>
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index a974bd5..beb73df 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -15,7 +15,7 @@ OBJS = catalog.o dependency.o heap.o index.o indexing.o namespace.o aclchk.o \
pg_constraint.o pg_conversion.o \
pg_depend.o pg_enum.o pg_inherits.o pg_largeobject.o pg_namespace.o \
pg_operator.o pg_proc.o pg_range.o pg_db_role_setting.o pg_shdepend.o \
- pg_type.o storage.o toasting.o
+ pg_rowsecurity.o pg_type.o storage.o toasting.o
BKIFILES = postgres.bki postgres.description postgres.shdescription
@@ -39,7 +39,7 @@ POSTGRES_BKI_SRCS = $(addprefix $(top_srcdir)/src/include/catalog/,\
pg_ts_config.h pg_ts_config_map.h pg_ts_dict.h \
pg_ts_parser.h pg_ts_template.h pg_extension.h \
pg_foreign_data_wrapper.h pg_foreign_server.h pg_user_mapping.h \
- pg_foreign_table.h \
+ pg_foreign_table.h pg_rowsecurity.h \
pg_default_acl.h pg_seclabel.h pg_shseclabel.h pg_collation.h pg_range.h \
toasting.h indexing.h \
)
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index e511669..8f95b14 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_rowsecurity.h"
#include "catalog/pg_tablespace.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_ts_config.h"
@@ -1249,6 +1250,10 @@ doDeletion(const ObjectAddress *object, int flags)
RemoveEventTriggerById(object->objectId);
break;
+ case OCLASS_ROWSECURITY:
+ RemoveRowSecurityById(object->objectId);
+ break;
+
default:
elog(ERROR, "unrecognized object class: %u",
object->classId);
@@ -2316,6 +2321,9 @@ getObjectClass(const ObjectAddress *object)
case EventTriggerRelationId:
return OCLASS_EVENT_TRIGGER;
+
+ case RowSecurityRelationId:
+ return OCLASS_ROWSECURITY;
}
/* shouldn't get here */
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 2cf4bc0..5be195b 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -799,6 +799,7 @@ InsertPgClassTuple(Relation pg_class_desc,
values[Anum_pg_class_relhaspkey - 1] = BoolGetDatum(rd_rel->relhaspkey);
values[Anum_pg_class_relhasrules - 1] = BoolGetDatum(rd_rel->relhasrules);
values[Anum_pg_class_relhastriggers - 1] = BoolGetDatum(rd_rel->relhastriggers);
+ values[Anum_pg_class_relhasrowsecurity - 1] = BoolGetDatum(rd_rel->relhasrowsecurity);
values[Anum_pg_class_relhassubclass - 1] = BoolGetDatum(rd_rel->relhassubclass);
values[Anum_pg_class_relispopulated - 1] = BoolGetDatum(rd_rel->relispopulated);
values[Anum_pg_class_relreplident - 1] = CharGetDatum(rd_rel->relreplident);
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 991c02a..870e477 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -2143,6 +2143,56 @@ getObjectDescription(const ObjectAddress *object)
break;
}
+ case OCLASS_ROWSECURITY:
+ {
+ Relation rsec_rel;
+ ScanKeyData skey;
+ SysScanDesc sscan;
+ HeapTuple tuple;
+ Form_pg_rowsecurity form_rsec;
+
+ rsec_rel = heap_open(RowSecurityRelationId, AccessShareLock);
+
+ ScanKeyInit(&skey,
+ ObjectIdAttributeNumber,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(object->objectId));
+ sscan = systable_beginscan(rsec_rel, RowSecurityOidIndexId,
+ true, NULL, 1, &skey);
+ tuple = systable_getnext(sscan);
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "could not find tuple for row-security %u",
+ object->objectId);
+ form_rsec = (Form_pg_rowsecurity) GETSTRUCT(tuple);
+
+ appendStringInfo(&buffer, _("row-security of "));
+ getRelationDescription(&buffer, form_rsec->rsecrelid);
+ switch (form_rsec->rseccmd)
+ {
+ case ROWSECURITY_CMD_ALL:
+ appendStringInfo(&buffer, _(" FOR ALL"));
+ break;
+ case ROWSECURITY_CMD_SELECT:
+ appendStringInfo(&buffer, _(" FOR SELECT"));
+ break;
+ case ROWSECURITY_CMD_INSERT:
+ appendStringInfo(&buffer, _(" FOR INSERT"));
+ break;
+ case ROWSECURITY_CMD_UPDATE:
+ appendStringInfo(&buffer, _(" FOR UPDATE"));
+ break;
+ case ROWSECURITY_CMD_DELETE:
+ appendStringInfo(&buffer, _(" FOR DELETE"));
+ break;
+ default:
+ elog(ERROR, "unexpected row-security command type: %c",
+ form_rsec->rseccmd);
+ }
+ systable_endscan(sscan);
+ heap_close(rsec_rel, AccessShareLock);
+ break;
+ }
+
default:
appendStringInfo(&buffer, "unrecognized object %u %u %d",
object->classId,
diff --git a/src/backend/catalog/pg_rowsecurity.c b/src/backend/catalog/pg_rowsecurity.c
new file mode 100644
index 0000000..7ee4e88
--- /dev/null
+++ b/src/backend/catalog/pg_rowsecurity.c
@@ -0,0 +1,287 @@
+/* -------------------------------------------------------------------------
+ *
+ * pg_rowsecurity.c
+ * routines to support manipulation of the pg_rowsecurity catalog
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * -------------------------------------------------------------------------
+ */
+#include "postgres.h"
+#include "access/genam.h"
+#include "access/heapam.h"
+#include "access/htup_details.h"
+#include "access/sysattr.h"
+#include "catalog/dependency.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_rowsecurity.h"
+#include "catalog/pg_type.h"
+#include "nodes/nodeFuncs.h"
+#include "optimizer/clauses.h"
+#include "parser/parse_clause.h"
+#include "parser/parse_node.h"
+#include "parser/parse_relation.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/inval.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/tqual.h"
+
+/*
+ * Load row-security policy from the catalog, and keep it on
+ * the relation cache.
+ */
+void
+RelationBuildRowSecurity(Relation relation)
+{
+ Relation catalog;
+ ScanKeyData skey;
+ SysScanDesc sscan;
+ HeapTuple tuple;
+ MemoryContext oldcxt;
+ MemoryContext rscxt = NULL;
+ RowSecurityDesc *rsdesc = NULL;
+
+ catalog = heap_open(RowSecurityRelationId, AccessShareLock);
+
+ ScanKeyInit(&skey,
+ Anum_pg_rowsecurity_rsecrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(RelationGetRelid(relation)));
+ sscan = systable_beginscan(catalog, RowSecurityRelidIndexId, true,
+ NULL, 1, &skey);
+ PG_TRY();
+ {
+ while (HeapTupleIsValid(tuple = systable_getnext(sscan)))
+ {
+ Datum value;
+ bool isnull;
+ char *temp;
+
+ if (!rsdesc)
+ {
+ rscxt = AllocSetContextCreate(CacheMemoryContext,
+ "Row-security descriptor",
+ ALLOCSET_SMALL_MINSIZE,
+ ALLOCSET_SMALL_INITSIZE,
+ ALLOCSET_SMALL_MAXSIZE);
+ oldcxt = MemoryContextSwitchTo(rscxt);
+ rsdesc = palloc0(sizeof(RowSecurityDesc));
+ rsdesc->rscxt = rscxt;
+ MemoryContextSwitchTo(oldcxt);
+ }
+ value = heap_getattr(tuple, Anum_pg_rowsecurity_rseccmd,
+ RelationGetDescr(catalog), &isnull);
+ Assert(!isnull);
+
+ if (DatumGetChar(value) != ROWSECURITY_CMD_ALL)
+ {
+ ereport(WARNING,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("Per-command row-security not implemented")));
+ continue;
+ }
+
+ value = heap_getattr(tuple, Anum_pg_rowsecurity_rsecqual,
+ RelationGetDescr(catalog), &isnull);
+ Assert(!isnull);
+ temp = TextDatumGetCString(value);
+
+ oldcxt = MemoryContextSwitchTo(rscxt);
+ rsdesc->rsall.rsecid = HeapTupleGetOid(tuple);
+ rsdesc->rsall.qual = (Expr *) stringToNode(temp);
+ Assert(exprType((Node *)rsdesc->rsall.qual) == BOOLOID);
+ rsdesc->rsall.hassublinks
+ = contain_subplans((Node *)rsdesc->rsall.qual);
+ MemoryContextSwitchTo(oldcxt);
+
+ pfree(temp);
+ }
+ }
+ PG_CATCH();
+ {
+ if (rscxt != NULL)
+ MemoryContextDelete(rscxt);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ systable_endscan(sscan);
+ heap_close(catalog, AccessShareLock);
+
+ relation->rsdesc = rsdesc;
+}
+
+/*
+ * Parse the supplied row-security policy, and insert/update a row
+ * of pg_rowsecurity catalog.
+ */
+static void
+InsertOrUpdatePolicyRow(Relation relation, char rseccmd, Node *clause)
+{
+ Oid relationId = RelationGetRelid(relation);
+ Oid rowsecId;
+ ParseState *pstate;
+ RangeTblEntry *rte;
+ Node *qual;
+ Relation catalog;
+ ScanKeyData skeys[2];
+ SysScanDesc sscan;
+ HeapTuple oldtup;
+ HeapTuple newtup;
+ Datum values[Natts_pg_rowsecurity];
+ bool isnull[Natts_pg_rowsecurity];
+ bool replaces[Natts_pg_rowsecurity];
+ ObjectAddress target;
+ ObjectAddress myself;
+
+ /* Parse the supplied clause */
+ pstate = make_parsestate(NULL);
+
+ rte = addRangeTableEntryForRelation(pstate, relation,
+ NULL, false, false);
+ addRTEtoQuery(pstate, rte, false, true, true);
+
+ qual = transformWhereClause(pstate, copyObject(clause),
+ EXPR_KIND_ROW_SECURITY,
+ "ROW SECURITY");
+ /* zero-clear */
+ memset(values, 0, sizeof(values));
+ memset(replaces, 0, sizeof(replaces));
+ memset(isnull, 0, sizeof(isnull));
+
+ /* Update or Insert an entry to pg_rowsecurity catalog */
+ catalog = heap_open(RowSecurityRelationId, RowExclusiveLock);
+
+ ScanKeyInit(&skeys[0],
+ Anum_pg_rowsecurity_rsecrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(RelationGetRelid(relation)));
+ ScanKeyInit(&skeys[1],
+ Anum_pg_rowsecurity_rseccmd,
+ BTEqualStrategyNumber, F_CHAREQ,
+ CharGetDatum(rseccmd));
+ sscan = systable_beginscan(catalog, RowSecurityRelidIndexId, true,
+ NULL, 2, skeys);
+ oldtup = systable_getnext(sscan);
+ if (HeapTupleIsValid(oldtup))
+ {
+ rowsecId = HeapTupleGetOid(oldtup);
+
+ replaces[Anum_pg_rowsecurity_rsecqual - 1] = true;
+ values[Anum_pg_rowsecurity_rsecqual - 1]
+ = CStringGetTextDatum(nodeToString(qual));
+
+ newtup = heap_modify_tuple(oldtup,
+ RelationGetDescr(catalog),
+ values, isnull, replaces);
+ simple_heap_update(catalog, &newtup->t_self, newtup);
+
+ deleteDependencyRecordsFor(RowSecurityRelationId, rowsecId, false);
+ }
+ else
+ {
+ values[Anum_pg_rowsecurity_rsecrelid - 1]
+ = ObjectIdGetDatum(relationId);
+ values[Anum_pg_rowsecurity_rseccmd - 1]
+ = CharGetDatum(rseccmd);
+ values[Anum_pg_rowsecurity_rsecqual - 1]
+ = CStringGetTextDatum(nodeToString(qual));
+ newtup = heap_form_tuple(RelationGetDescr(catalog),
+ values, isnull);
+ rowsecId = simple_heap_insert(catalog, newtup);
+ }
+ CatalogUpdateIndexes(catalog, newtup);
+
+ heap_freetuple(newtup);
+
+ /* records dependencies of row-security policy and relation/columns */
+ target.classId = RelationRelationId;
+ target.objectId = relationId;
+ target.objectSubId = 0;
+
+ myself.classId = RowSecurityRelationId;
+ myself.objectId = rowsecId;
+ myself.objectSubId = 0;
+
+ recordDependencyOn(&myself, &target, DEPENDENCY_AUTO);
+
+ recordDependencyOnExpr(&myself, qual, pstate->p_rtable,
+ DEPENDENCY_NORMAL);
+ free_parsestate(pstate);
+
+ systable_endscan(sscan);
+ heap_close(catalog, RowExclusiveLock);
+}
+
+/*
+ * Remove row-security policy row of pg_rowsecurity
+ */
+static void
+DeletePolicyRow(Relation relation, char rseccmd)
+{
+ Assert(rseccmd == ROWSECURITY_CMD_ALL);
+
+ if (relation->rsdesc)
+ {
+ ObjectAddress address;
+
+ address.classId = RowSecurityRelationId;
+ address.objectId = relation->rsdesc->rsall.rsecid;
+ address.objectSubId = 0;
+
+ performDeletion(&address, DROP_RESTRICT, 0);
+ }
+ else
+ {
+ /* Nothing to do here */
+ elog(INFO, "relation %s has no row-security policy, skipped",
+ RelationGetRelationName(relation));
+ }
+}
+
+/*
+ * Guts of row-security policy deletion.
+ */
+void
+RemoveRowSecurityById(Oid rowsecId)
+{
+ Relation catalog;
+ ScanKeyData skey;
+ SysScanDesc sscan;
+ HeapTuple tuple;
+ Relation rel;
+ Oid relid;
+
+ catalog = heap_open(RowSecurityRelationId, RowExclusiveLock);
+
+ ScanKeyInit(&skey,
+ ObjectIdAttributeNumber,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(rowsecId));
+ sscan = systable_beginscan(catalog, RowSecurityOidIndexId, true,
+ NULL, 1, &skey);
+ tuple = systable_getnext(sscan);
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "could not find tuple for row-security %u", rowsecId);
+
+ /*
+ * Open and exclusive-lock the relation the row-security belongs to.
+ */
+ relid = ((Form_pg_rowsecurity) GETSTRUCT(tuple))->rsecrelid;
+
+ rel = heap_open(relid, AccessExclusiveLock);
+
+ simple_heap_delete(catalog, &tuple->t_self);
+
+ /* Ensure relcache entries of other session being rebuilt */
+ CacheInvalidateRelcache(rel);
+
+ heap_close(rel, NoLock);
+
+ systable_endscan(sscan);
+ heap_close(catalog, RowExclusiveLock);
+}
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 2a46cfc..8ee5353 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -50,6 +50,7 @@
#include "catalog/pg_opclass.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_rewrite.h"
+#include "catalog/pg_rowsecurity.h"
#include "catalog/pg_tablespace.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
@@ -932,6 +933,11 @@ RelationBuildDesc(Oid targetRelId, bool insertIt)
else
relation->trigdesc = NULL;
+ if (relation->rd_rel->relhasrowsecurity)
+ RelationBuildRowSecurity(relation);
+ else
+ relation->rsdesc = NULL;
+
/*
* if it's an index, initialize index-related information
*/
@@ -1840,6 +1846,8 @@ RelationDestroyRelation(Relation relation)
MemoryContextDelete(relation->rd_indexcxt);
if (relation->rd_rulescxt)
MemoryContextDelete(relation->rd_rulescxt);
+ if (relation->rsdesc)
+ MemoryContextDelete(relation->rsdesc->rscxt);
if (relation->rd_fdwroutine)
pfree(relation->rd_fdwroutine);
pfree(relation);
@@ -3166,7 +3174,13 @@ RelationCacheInitializePhase3(void)
relation->rd_rel->relhastriggers = false;
restart = true;
}
-
+ if (relation->rd_rel->relhasrowsecurity && relation->rsdesc == NULL)
+ {
+ RelationBuildRowSecurity(relation);
+ if (relation->rsdesc == NULL)
+ relation->rd_rel->relhasrowsecurity = false;
+ restart = true;
+ }
/* Release hold on the relation */
RelationDecrementReferenceCount(relation);
@@ -4478,6 +4492,7 @@ load_relcache_init_file(bool shared)
rel->rd_rules = NULL;
rel->rd_rulescxt = NULL;
rel->trigdesc = NULL;
+ rel->rsdesc = NULL;
rel->rd_indexprs = NIL;
rel->rd_indpred = NIL;
rel->rd_exclops = NULL;
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index 8948589..de9323e 100644
--- a/src/include/catalog/dependency.h
+++ b/src/include/catalog/dependency.h
@@ -147,6 +147,7 @@ typedef enum ObjectClass
OCLASS_DEFACL, /* pg_default_acl */
OCLASS_EXTENSION, /* pg_extension */
OCLASS_EVENT_TRIGGER, /* pg_event_trigger */
+ OCLASS_ROWSECURITY, /* pg_rowsecurity */
MAX_OCLASS /* MUST BE LAST */
} ObjectClass;
diff --git a/src/include/catalog/indexing.h b/src/include/catalog/indexing.h
index 0515b75..4f46c5d 100644
--- a/src/include/catalog/indexing.h
+++ b/src/include/catalog/indexing.h
@@ -313,6 +313,11 @@ DECLARE_UNIQUE_INDEX(pg_extension_name_index, 3081, on pg_extension using btree(
DECLARE_UNIQUE_INDEX(pg_range_rngtypid_index, 3542, on pg_range using btree(rngtypid oid_ops));
#define RangeTypidIndexId 3542
+DECLARE_UNIQUE_INDEX(pg_rowsecurity_oid_index, 5001, on pg_rowsecurity using btree(oid oid_ops));
+#define RowSecurityOidIndexId 5001
+DECLARE_UNIQUE_INDEX(pg_rowsecurity_relid_index, 5002, on pg_rowsecurity using btree(rsecrelid oid_ops, rseccmd char_ops));
+#define RowSecurityRelidIndexId 5002
+
/* last step of initialization script: build the indexes declared above */
BUILD_INDICES
diff --git a/src/include/catalog/pg_class.h b/src/include/catalog/pg_class.h
index f2fb317..bbbf62d 100644
--- a/src/include/catalog/pg_class.h
+++ b/src/include/catalog/pg_class.h
@@ -65,6 +65,7 @@ CATALOG(pg_class,1259) BKI_BOOTSTRAP BKI_ROWTYPE_OID(83) BKI_SCHEMA_MACRO
bool relhasrules; /* has (or has had) any rules */
bool relhastriggers; /* has (or has had) any TRIGGERs */
bool relhassubclass; /* has (or has had) derived classes */
+ bool relhasrowsecurity; /* has (or has had) row-security policy */
bool relispopulated; /* matview currently holds query results */
char relreplident; /* see REPLICA_IDENTITY_xxx constants */
TransactionId relfrozenxid; /* all Xids < this are frozen in this rel */
@@ -94,7 +95,7 @@ typedef FormData_pg_class *Form_pg_class;
* ----------------
*/
-#define Natts_pg_class 29
+#define Natts_pg_class 30
#define Anum_pg_class_relname 1
#define Anum_pg_class_relnamespace 2
#define Anum_pg_class_reltype 3
@@ -118,12 +119,13 @@ typedef FormData_pg_class *Form_pg_class;
#define Anum_pg_class_relhasrules 21
#define Anum_pg_class_relhastriggers 22
#define Anum_pg_class_relhassubclass 23
-#define Anum_pg_class_relispopulated 24
-#define Anum_pg_class_relreplident 25
-#define Anum_pg_class_relfrozenxid 26
-#define Anum_pg_class_relminmxid 27
-#define Anum_pg_class_relacl 28
-#define Anum_pg_class_reloptions 29
+#define Anum_pg_class_relhasrowsecurity 24
+#define Anum_pg_class_relispopulated 25
+#define Anum_pg_class_relreplident 26
+#define Anum_pg_class_relfrozenxid 27
+#define Anum_pg_class_relminmxid 28
+#define Anum_pg_class_relacl 29
+#define Anum_pg_class_reloptions 30
/* ----------------
* initial contents of pg_class
@@ -138,13 +140,14 @@ typedef FormData_pg_class *Form_pg_class;
* Note: "3" in the relfrozenxid column stands for FirstNormalTransactionId;
* similarly, "1" in relminmxid stands for FirstMultiXactId
*/
-DATA(insert OID = 1247 ( pg_type PGNSP 71 0 PGUID 0 0 0 0 0 0 0 f f p r 30 0 t f f f f t n 3 1 _null_ _null_ ));
+
+DATA(insert OID = 1247 ( pg_type PGNSP 71 0 PGUID 0 0 0 0 0 0 0 f f p r 30 0 t f f f f f t n 3 1 _null_ _null_ ));
DESCR("");
-DATA(insert OID = 1249 ( pg_attribute PGNSP 75 0 PGUID 0 0 0 0 0 0 0 f f p r 21 0 f f f f f t n 3 1 _null_ _null_ ));
+DATA(insert OID = 1249 ( pg_attribute PGNSP 75 0 PGUID 0 0 0 0 0 0 0 f f p r 21 0 f f f f f f t n 3 1 _null_ _null_ ));
DESCR("");
-DATA(insert OID = 1255 ( pg_proc PGNSP 81 0 PGUID 0 0 0 0 0 0 0 f f p r 27 0 t f f f f t n 3 1 _null_ _null_ ));
+DATA(insert OID = 1255 ( pg_proc PGNSP 81 0 PGUID 0 0 0 0 0 0 0 f f p r 27 0 t f f f f f t n 3 1 _null_ _null_ ));
DESCR("");
-DATA(insert OID = 1259 ( pg_class PGNSP 83 0 PGUID 0 0 0 0 0 0 0 f f p r 29 0 t f f f f t n 3 1 _null_ _null_ ));
+DATA(insert OID = 1259 ( pg_class PGNSP 83 0 PGUID 0 0 0 0 0 0 0 f f p r 30 0 t f f f f f t n 3 1 _null_ _null_ ));
DESCR("");
diff --git a/src/include/catalog/pg_rowsecurity.h b/src/include/catalog/pg_rowsecurity.h
new file mode 100644
index 0000000..10aed9a
--- /dev/null
+++ b/src/include/catalog/pg_rowsecurity.h
@@ -0,0 +1,74 @@
+/*
+ * pg_rowsecurity.h
+ * definition of the system catalog for row-security policy (pg_rowsecurity)
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ */
+#ifndef PG_ROWSECURITY_H
+#define PG_ROWSECURITY_H
+
+#include "catalog/genbki.h"
+#include "nodes/primnodes.h"
+#include "utils/memutils.h"
+#include "utils/relcache.h"
+
+/* ----------------
+ * pg_rowlevelsec definition. cpp turns this into
+ * typedef struct FormData_pg_rowlevelsec
+ * ----------------
+ */
+#define RowSecurityRelationId 5000
+
+CATALOG(pg_rowsecurity,5000)
+{
+ /* Oid of the relation that has row-security policy */
+ Oid rsecrelid;
+
+ /* One of ROWSECURITY_CMD_* below */
+ char rseccmd;
+#ifdef CATALOG_VARLEN
+ pg_node_tree rsecqual;
+#endif
+} FormData_pg_rowsecurity;
+
+/* ----------------
+ * Form_pg_rowlevelsec corresponds to a pointer to a row with
+ * the format of pg_rowlevelsec relation.
+ * ----------------
+ */
+typedef FormData_pg_rowsecurity *Form_pg_rowsecurity;
+
+/* ----------------
+ * compiler constants for pg_rowlevelsec
+ * ----------------
+ */
+#define Natts_pg_rowsecurity 3
+#define Anum_pg_rowsecurity_rsecrelid 1
+#define Anum_pg_rowsecurity_rseccmd 2
+#define Anum_pg_rowsecurity_rsecqual 3
+
+#define ROWSECURITY_CMD_ALL 'a'
+#define ROWSECURITY_CMD_SELECT 's'
+#define ROWSECURITY_CMD_INSERT 'i'
+#define ROWSECURITY_CMD_UPDATE 'u'
+#define ROWSECURITY_CMD_DELETE 'd'
+
+typedef struct
+{
+ Oid rsecid;
+ Expr *qual;
+ bool hassublinks;
+} RowSecurityEntry;
+
+typedef struct
+{
+ MemoryContext rscxt;
+ RowSecurityEntry rsall; /* row-security policy for ALL */
+} RowSecurityDesc;
+
+extern void RelationBuildRowSecurity(Relation relation);
+extern void RemoveRowSecurityById(Oid relationId);
+
+#endif /* PG_ROWSECURITY_H */
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index 85598e8..9c7c382 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -63,7 +63,8 @@ typedef enum ParseExprKind
EXPR_KIND_INDEX_PREDICATE, /* index predicate */
EXPR_KIND_ALTER_COL_TRANSFORM, /* transform expr in ALTER COLUMN TYPE */
EXPR_KIND_EXECUTE_PARAMETER, /* parameter value in EXECUTE */
- EXPR_KIND_TRIGGER_WHEN /* WHEN condition in CREATE TRIGGER */
+ EXPR_KIND_TRIGGER_WHEN, /* WHEN condition in CREATE TRIGGER */
+ EXPR_KIND_ROW_SECURITY /* ROW SECURITY policy for a table */
} ParseExprKind;
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 9b8a4c9..83a8d03 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -18,6 +18,7 @@
#include "catalog/pg_am.h"
#include "catalog/pg_class.h"
#include "catalog/pg_index.h"
+#include "catalog/pg_rowsecurity.h"
#include "fmgr.h"
#include "nodes/bitmapset.h"
#include "rewrite/prs2lock.h"
@@ -110,6 +111,7 @@ typedef struct RelationData
RuleLock *rd_rules; /* rewrite rules */
MemoryContext rd_rulescxt; /* private memory cxt for rd_rules, if any */
TriggerDesc *trigdesc; /* Trigger info, or NULL if rel has none */
+ RowSecurityDesc *rsdesc; /* Row-security policy, or NULL */
/*
* The index chosen as the relation's replication identity or
diff --git a/src/test/regress/expected/sanity_check.out b/src/test/regress/expected/sanity_check.out
index a62a3e3..a41344b 100644
--- a/src/test/regress/expected/sanity_check.out
+++ b/src/test/regress/expected/sanity_check.out
@@ -121,6 +121,7 @@ pg_pltemplate|t
pg_proc|t
pg_range|t
pg_rewrite|t
+pg_rowsecurity|t
pg_seclabel|t
pg_shdepend|t
pg_shdescription|t
--
1.8.3.1
0003-RLS-Add-rowsec_relid-to-ResultRelInfo.patchtext/x-patch; name=0003-RLS-Add-rowsec_relid-to-ResultRelInfo.patchDownload
>From 76847f695ce425117783e722dc49d22b1464ef64 Mon Sep 17 00:00:00 2001
From: Craig Ringer <craig@2ndquadrant.com>
Date: Mon, 20 Jan 2014 13:28:12 +0800
Subject: [PATCH 03/10] RLS: Add rowsec_relid to ResultRelInfo
This hunk may still be needed in the new approach to filter out rows that've
changed to no longer be visible due to user action r action of triggers;
we'd need to filter the result relation.
---
src/backend/nodes/copyfuncs.c | 1 +
src/backend/nodes/equalfuncs.c | 1 +
src/backend/nodes/outfuncs.c | 1 +
src/backend/nodes/readfuncs.c | 1 +
src/include/nodes/execnodes.h | 4 ++++
src/include/nodes/parsenodes.h | 8 +++++++-
6 files changed, 15 insertions(+), 1 deletion(-)
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index bb356d0..52bd8f5 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -1977,6 +1977,7 @@ _copyRangeTblEntry(const RangeTblEntry *from)
COPY_SCALAR_FIELD(relkind);
COPY_NODE_FIELD(subquery);
COPY_SCALAR_FIELD(security_barrier);
+ COPY_SCALAR_FIELD(rowsec_relid);
COPY_SCALAR_FIELD(jointype);
COPY_NODE_FIELD(joinaliasvars);
COPY_NODE_FIELD(functions);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 5908d9a..b9b0de2 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -2269,6 +2269,7 @@ _equalRangeTblEntry(const RangeTblEntry *a, const RangeTblEntry *b)
COMPARE_SCALAR_FIELD(relkind);
COMPARE_NODE_FIELD(subquery);
COMPARE_SCALAR_FIELD(security_barrier);
+ COMPARE_SCALAR_FIELD(rowsec_relid);
COMPARE_SCALAR_FIELD(jointype);
COMPARE_NODE_FIELD(joinaliasvars);
COMPARE_NODE_FIELD(functions);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 568c3b8..cfa4cb3 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -2376,6 +2376,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node)
case RTE_SUBQUERY:
WRITE_NODE_FIELD(subquery);
WRITE_BOOL_FIELD(security_barrier);
+ WRITE_OID_FIELD(rowsec_relid);
break;
case RTE_JOIN:
WRITE_ENUM_FIELD(jointype, JoinType);
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index c22acd5..335579f 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -1218,6 +1218,7 @@ _readRangeTblEntry(void)
case RTE_SUBQUERY:
READ_NODE_FIELD(subquery);
READ_BOOL_FIELD(security_barrier);
+ READ_OID_FIELD(rowsec_relid);
break;
case RTE_JOIN:
READ_ENUM_FIELD(jointype, JoinType);
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index a301a08..83637eb 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -308,6 +308,8 @@ typedef struct JunkFilter
* ConstraintExprs array of constraint-checking expr states
* junkFilter for removing junk attributes from tuples
* projectReturning for computing a RETURNING list
+ * rowSecurity for row-security checks
+ * rowSecParams param-list if row-security has SubLink
* ----------------
*/
typedef struct ResultRelInfo
@@ -329,6 +331,8 @@ typedef struct ResultRelInfo
List **ri_ConstraintExprs;
JunkFilter *ri_junkFilter;
ProjectionInfo *ri_projectReturning;
+ Node *ri_rowSecurity;
+ List *ri_rowSecParams;
} ResultRelInfo;
/* ----------------
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 846c31a..48c6d0f 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -31,7 +31,8 @@ typedef enum QuerySource
QSRC_PARSER, /* added by parse analysis (now unused) */
QSRC_INSTEAD_RULE, /* added by unconditional INSTEAD rule */
QSRC_QUAL_INSTEAD_RULE, /* added by conditional INSTEAD rule */
- QSRC_NON_INSTEAD_RULE /* added by non-INSTEAD rule */
+ QSRC_NON_INSTEAD_RULE, /* added by non-INSTEAD rule */
+ QSRC_ROW_SECURITY, /* added by row-security */
} QuerySource;
/* Sort ordering options for ORDER BY and CREATE INDEX */
@@ -741,6 +742,11 @@ typedef struct RangeTblEntry
*/
Query *subquery; /* the sub-query */
bool security_barrier; /* is from security_barrier view? */
+ Oid rowsec_relid; /* OID of the original relation, if this
+ * sub-query originated from row-security
+ * policy on the relation. Elsewhere, it
+ * should be InvalidOid.
+ */
/*
* Fields valid for a join RTE (else NULL/zero):
--
1.8.3.1
0004-Add-ALTER-TABLE-commands-for-row-security.patchtext/x-patch; name=0004-Add-ALTER-TABLE-commands-for-row-security.patchDownload
>From 8a2a621c72464f06f0633b019f62d5dd13f8a10b Mon Sep 17 00:00:00 2001
From: Craig Ringer <craig@2ndquadrant.com>
Date: Mon, 20 Jan 2014 13:37:47 +0800
Subject: [PATCH 04/10] Add ALTER TABLE commands for row security
---
doc/src/sgml/ref/alter_table.sgml | 38 +++++++++++++++++++++++++++
src/backend/catalog/pg_rowsecurity.c | 50 ++++++++++++++++++++++++++++++++++++
src/backend/commands/event_trigger.c | 1 +
src/backend/commands/tablecmds.c | 27 +++++++++++++++++++
src/backend/parser/gram.y | 25 ++++++++++++++++++
src/backend/parser/parse_agg.c | 6 +++++
src/backend/parser/parse_expr.c | 3 +++
src/include/catalog/pg_rowsecurity.h | 2 ++
src/include/nodes/parsenodes.h | 2 ++
9 files changed, 154 insertions(+)
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 89649a2..79f37d8 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -69,6 +69,8 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable>
NOT OF
OWNER TO <replaceable class="PARAMETER">new_owner</replaceable>
SET TABLESPACE <replaceable class="PARAMETER">new_tablespace</replaceable>
+ SET ROW SECURITY FOR <replaceable class="PARAMETER">rowsec_command</replaceable> TO (<replaceable class="PARAMETER">condition</replaceable>)
+ RESET ROW SECURITY FOR <replaceable class="PARAMETER">rowsec_command</replaceable>
REPLICA IDENTITY {DEFAULT | USING INDEX <replaceable class="PARAMETER">index_name</replaceable> | FULL | NOTHING}
<phrase>and <replaceable class="PARAMETER">table_constraint_using_index</replaceable> is:</phrase>
@@ -76,6 +78,8 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable>
[ CONSTRAINT <replaceable class="PARAMETER">constraint_name</replaceable> ]
{ UNIQUE | PRIMARY KEY } USING INDEX <replaceable class="PARAMETER">index_name</replaceable>
[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+<phrase>and <replaceable class="PARAMETER">rowsec_command</replaceable> is:</phrase>
+ { ALL | SELECT | INSERT | UPDATE | DELETE }
</synopsis>
</refsynopsisdiv>
@@ -581,6 +585,26 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable>
</varlistentry>
<varlistentry>
+ <term><literal>SET ROW SECURITY FOR <replaceable class="PARAMETER">rowsec_command</replaceable> TO (<replaceable class="PARAMETER">condition</replaceable>)</literal></term>
+ <listitem>
+ <para>
+ This form set row-level security policy of the table.
+ Supplied <replaceable class="PARAMETER">condition</replaceable> performs
+ as if it is implicitly appended to the qualifiers of <literal>WHERE</literal>
+ clause, although mechanism guarantees to evaluate this condition earlier
+ than any other user given condition.
+ <literal>ALL</> is the only supported command type right now.
+ See also <xref linkend="ROW-SECURITY">.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>RESET ROW SECURITY FOR <replaceable class="PARAMETER">rowsec_command</replaceable></literal></term>
+ <listitem>
+ <para>
+ This form reset row-level security policy of the table, if exists.
+ <literal>ALL</> is the only supported command type right now.
<term><literal>REPLICA IDENTITY</literal></term>
<listitem>
<para>
@@ -840,6 +864,20 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable>
</listitem>
</varlistentry>
+ <varlistentry>
+ <term><replaceable class="PARAMETER">condition</replaceable></term>
+ <listitem>
+ <para>
+ An expression that returns a value of type boolean. Expect for a case
+ when queries are executed with superuser privilege, only rows for which
+ this expression returns true will be fetched, updated or deleted.
+ This expression can reference columns of the relation being configured.
+ Sub-queries can be contained within expression tree, unless referenced
+ relation recursively references the same relation.
+ </para>
+ </listitem>
+ </varlistentry>
+
</variablelist>
</refsect1>
diff --git a/src/backend/catalog/pg_rowsecurity.c b/src/backend/catalog/pg_rowsecurity.c
index 7ee4e88..34d33e8 100644
--- a/src/backend/catalog/pg_rowsecurity.c
+++ b/src/backend/catalog/pg_rowsecurity.c
@@ -285,3 +285,53 @@ RemoveRowSecurityById(Oid rowsecId)
systable_endscan(sscan);
heap_close(catalog, RowExclusiveLock);
}
+
+/*
+ * ALTER TABLE <name> SET ROW SECURITY (...) OR
+ * RESET ROW SECURITY
+ */
+void
+ATExecSetRowSecurity(Relation relation, const char *cmdname, Node *clause)
+{
+ Oid relid = RelationGetRelid(relation);
+ char rseccmd;
+
+ if (strcmp(cmdname, "all") == 0)
+ rseccmd = ROWSECURITY_CMD_ALL;
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("Row-security for \"%s\" is not implemented yet",
+ cmdname)));
+
+ if (clause != NULL)
+ {
+ InsertOrUpdatePolicyRow(relation, rseccmd, clause);
+
+ /*
+ * Also, turn on relhasrowsecurity, if not.
+ */
+ if (!RelationGetForm(relation)->relhasrowsecurity)
+ {
+ Relation class_rel = heap_open(RelationRelationId,
+ RowExclusiveLock);
+ HeapTuple tuple;
+
+ tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for relation %u", relid);
+
+ ((Form_pg_class) GETSTRUCT(tuple))->relhasrowsecurity = true;
+
+ simple_heap_update(class_rel, &tuple->t_self, tuple);
+ CatalogUpdateIndexes(class_rel, tuple);
+
+ heap_freetuple(tuple);
+ heap_close(class_rel, RowExclusiveLock);
+ }
+ }
+ else
+ DeletePolicyRow(relation, rseccmd);
+
+ CacheInvalidateRelcache(relation);
+}
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 59f0842..51a1c0f 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -995,6 +995,7 @@ EventTriggerSupportsObjectClass(ObjectClass objclass)
case OCLASS_USER_MAPPING:
case OCLASS_DEFACL:
case OCLASS_EXTENSION:
+ case OCLASS_ROWSECURITY:
return true;
case MAX_OCLASS:
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 26a4613..f02ce72 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -37,6 +37,7 @@
#include "catalog/pg_inherits_fn.h"
#include "catalog/pg_namespace.h"
#include "catalog/pg_opclass.h"
+#include "catalog/pg_rowsecurity.h"
#include "catalog/pg_tablespace.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
@@ -2789,6 +2790,8 @@ AlterTableGetLockLevel(List *cmds)
case AT_SetTableSpace: /* must rewrite heap */
case AT_DropNotNull: /* may change some SQL plans */
case AT_SetNotNull:
+ case AT_SetRowSecurity:
+ case AT_ResetRowSecurity:
case AT_GenericOptions:
case AT_AlterColumnGenericOptions:
cmd_lockmode = AccessExclusiveLock;
@@ -3164,6 +3167,8 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
case AT_DropInherit: /* NO INHERIT */
case AT_AddOf: /* OF */
case AT_DropOf: /* NOT OF */
+ case AT_SetRowSecurity:
+ case AT_ResetRowSecurity:
ATSimplePermissions(rel, ATT_TABLE);
/* These commands never recurse */
/* No command-specific prep needed */
@@ -3449,6 +3454,12 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab, Relation rel,
case AT_DropOf:
ATExecDropOf(rel, lockmode);
break;
+ case AT_SetRowSecurity:
+ ATExecSetRowSecurity(rel, cmd->name, (Node *) cmd->def);
+ break;
+ case AT_ResetRowSecurity:
+ ATExecSetRowSecurity(rel, cmd->name, NULL);
+ break;
case AT_ReplicaIdentity:
ATExecReplicaIdentity(rel, (ReplicaIdentityStmt *) cmd->def, lockmode);
break;
@@ -7814,6 +7825,22 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
Assert(defaultexpr);
break;
+ case OCLASS_ROWSECURITY:
+ /*
+ * A row-level security policy can depend on a column in case
+ * when the policy clause references a particular column.
+ * Due to same reason why TRIGGER ... WHEN does not support
+ * to change column's type being referenced in clause, row-
+ * level security policy also does not support it.
+ */
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot alter type of a column used in a row-level security policy"),
+ errdetail("%s depends on column \"%s\"",
+ getObjectDescription(&foundObject),
+ colName)));
+ break;
+
case OCLASS_PROC:
case OCLASS_TYPE:
case OCLASS_CAST:
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 12a6beb..ba1ac81 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -260,6 +260,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
%type <node> alter_table_cmd alter_type_cmd opt_collate_clause
replica_identity
%type <list> alter_table_cmds alter_type_cmds
+%type <str> row_security_cmd
%type <dbehavior> opt_drop_behavior
@@ -2191,6 +2192,24 @@ alter_table_cmd:
n->def = (Node *)$2;
$$ = (Node *)n;
}
+ /* ALTER TABLE <name> SET ROW SECURITY FOR <cmd> TO (<expr>) */
+ | SET ROW SECURITY FOR row_security_cmd TO '(' a_expr ')'
+ {
+ AlterTableCmd *n = makeNode(AlterTableCmd);
+ n->subtype = AT_SetRowSecurity;
+ n->name = $5;
+ n->def = (Node *) $8;
+ $$ = (Node *)n;
+ }
+ /* ALTER TABLE <name> RESET ROW SECURITY FOR <cmd> */
+ | RESET ROW SECURITY FOR row_security_cmd
+ {
+ AlterTableCmd *n = makeNode(AlterTableCmd);
+ n->subtype = AT_ResetRowSecurity;
+ n->name = $5;
+ n->def = NULL;
+ $$ = (Node *)n;
+ }
/* ALTER TABLE <name> REPLICA IDENTITY */
| REPLICA IDENTITY_P replica_identity
{
@@ -2301,6 +2320,12 @@ reloption_elem:
}
;
+row_security_cmd: ALL { $$ = "all"; }
+ | SELECT { $$ = "select"; }
+ | INSERT { $$ = "insert"; }
+ | UPDATE { $$ = "update"; }
+ | DELETE_P { $$ = "delete"; }
+ ;
/*****************************************************************************
*
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
index 9613e2a..333b557 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -333,6 +333,9 @@ transformAggregateCall(ParseState *pstate, Aggref *agg,
case EXPR_KIND_TRIGGER_WHEN:
err = _("aggregate functions are not allowed in trigger WHEN conditions");
break;
+ case EXPR_KIND_ROW_SECURITY:
+ err = _("aggregate functions are not allowed in row-security policy");
+ break;
/*
* There is intentionally no default: case here, so that the
@@ -662,6 +665,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
case EXPR_KIND_TRIGGER_WHEN:
err = _("window functions are not allowed in trigger WHEN conditions");
break;
+ case EXPR_KIND_ROW_SECURITY:
+ err = _("window functions are not allowed in row-security policy");
+ break;
/*
* There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 81c9338..e289e77 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -1462,6 +1462,7 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
case EXPR_KIND_OFFSET:
case EXPR_KIND_RETURNING:
case EXPR_KIND_VALUES:
+ case EXPR_KIND_ROW_SECURITY:
/* okay */
break;
case EXPR_KIND_CHECK_CONSTRAINT:
@@ -2643,6 +2644,8 @@ ParseExprKindName(ParseExprKind exprKind)
return "EXECUTE";
case EXPR_KIND_TRIGGER_WHEN:
return "WHEN";
+ case EXPR_KIND_ROW_SECURITY:
+ return "ROW SECURITY";
/*
* There is intentionally no default: case here, so that the
diff --git a/src/include/catalog/pg_rowsecurity.h b/src/include/catalog/pg_rowsecurity.h
index 10aed9a..798d556 100644
--- a/src/include/catalog/pg_rowsecurity.h
+++ b/src/include/catalog/pg_rowsecurity.h
@@ -69,6 +69,8 @@ typedef struct
} RowSecurityDesc;
extern void RelationBuildRowSecurity(Relation relation);
+extern void ATExecSetRowSecurity(Relation relation,
+ const char *cmdname, Node *clause);
extern void RemoveRowSecurityById(Oid relationId);
#endif /* PG_ROWSECURITY_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 48c6d0f..3a918bb 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -1322,6 +1322,8 @@ typedef enum AlterTableType
AT_AddOf, /* OF <type_name> */
AT_DropOf, /* NOT OF */
AT_ReplicaIdentity, /* REPLICA IDENTITY */
+ AT_SetRowSecurity, /* SET ROW SECURITY (...) */
+ AT_ResetRowSecurity, /* RESET ROW SECURITY */
AT_GenericOptions /* OPTIONS (...) */
} AlterTableType;
--
1.8.3.1
0005-RLS-pg_dump-support-for-dumping-rowsecurity-catalogs.patchtext/x-patch; name=0005-RLS-pg_dump-support-for-dumping-rowsecurity-catalogs.patchDownload
>From bb8455bcf9dbaf5ef9cdf2898efcfde6eb6edecf Mon Sep 17 00:00:00 2001
From: Craig Ringer <craig@2ndquadrant.com>
Date: Mon, 20 Jan 2014 13:45:56 +0800
Subject: [PATCH 05/10] RLS: pg_dump support for dumping rowsecurity catalogs
---
src/bin/pg_dump/common.c | 4 +
src/bin/pg_dump/pg_backup_archiver.c | 1 +
src/bin/pg_dump/pg_dump.c | 151 ++++++++++++++++++++++++++++++++++-
src/bin/pg_dump/pg_dump.h | 13 ++-
src/bin/pg_dump/pg_dump_sort.c | 5 ++
5 files changed, 169 insertions(+), 5 deletions(-)
diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c
index 4d35ae5..fd9e6cd 100644
--- a/src/bin/pg_dump/common.c
+++ b/src/bin/pg_dump/common.c
@@ -244,6 +244,10 @@ getSchemaData(Archive *fout, int *numTablesPtr)
write_msg(NULL, "reading rewrite rules\n");
getRules(fout, &numRules);
+ if (g_verbose)
+ write_msg(NULL, "reading row-security policies\n");
+ getRowSecurity(fout, tblinfo, numTables);
+
*numTablesPtr = numTables;
return tblinfo;
}
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 7fc0288..6502d93 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3137,6 +3137,7 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, RestoreOptions *ropt, bool isDat
strcmp(te->desc, "INDEX") == 0 ||
strcmp(te->desc, "RULE") == 0 ||
strcmp(te->desc, "TRIGGER") == 0 ||
+ strcmp(te->desc, "ROW SECURITY") == 0 ||
strcmp(te->desc, "USER MAPPING") == 0)
{
/* these object types don't have separate owners */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 3862f05..6939852 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -250,6 +250,7 @@ static char *myFormatType(const char *typname, int32 typmod);
static void getBlobs(Archive *fout);
static void dumpBlob(Archive *fout, BlobInfo *binfo);
static int dumpBlobs(Archive *fout, void *arg);
+static void dumpRowSecurity(Archive *fout, RowSecurityInfo *rsinfo);
static void dumpDatabase(Archive *AH);
static void dumpEncoding(Archive *AH);
static void dumpStdStrings(Archive *AH);
@@ -2714,6 +2715,134 @@ dumpBlobs(Archive *fout, void *arg)
return 1;
}
+/*
+ * getRowSecurity
+ * get information about every row-security policy on a dumpable table
+ */
+void
+getRowSecurity(Archive *fout, TableInfo tblinfo[], int numTables)
+{
+ PQExpBuffer query = createPQExpBuffer();
+ PGresult *res;
+ RowSecurityInfo *rsinfo;
+ int i_oid;
+ int i_tableoid;
+ int i_rseccmd;
+ int i_rsecqual;
+ int i, j, ntups;
+
+ /* row-security is not supported prior to v9.4 */
+ if (fout->remoteVersion < 90400)
+ return;
+
+ for (i=0; i < numTables; i++)
+ {
+ TableInfo *tbinfo = &tblinfo[i];
+
+ if (!tbinfo->hasrowsec || !tbinfo->dobj.dump)
+ continue;
+
+ if (g_verbose)
+ write_msg(NULL, "reading row-security policy for table \"%s\"\n",
+ tbinfo->dobj.name);
+
+ /*
+ * select table schema to ensure regproc name is qualified if needed
+ */
+ selectSourceSchema(fout, tbinfo->dobj.namespace->dobj.name);
+
+ resetPQExpBuffer(query);
+
+ appendPQExpBuffer(query,
+ "SELECT oid, tableoid, s.rseccmd, "
+ "pg_get_expr(s.rsecqual, s.rsecrelid) AS rsecqual "
+ "FROM pg_catalog.pg_rowsecurity s "
+ "WHERE rsecrelid = '%u'",
+ tbinfo->dobj.catId.oid);
+ res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+
+ ntups = PQntuples(res);
+
+ i_oid = PQfnumber(res, "oid");
+ i_tableoid = PQfnumber(res, "tableoid");
+ i_rseccmd = PQfnumber(res, "rseccmd");
+ i_rsecqual = PQfnumber(res, "rsecqual");
+
+ rsinfo = pg_malloc(ntups * sizeof(RowSecurityInfo));
+ for (j=0; j < ntups; j++)
+ {
+ char namebuf[NAMEDATALEN + 1];
+
+ rsinfo[j].dobj.objType = DO_ROW_SECURITY;
+ rsinfo[j].dobj.catId.tableoid =
+ atooid(PQgetvalue(res, j, i_tableoid));
+ rsinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
+ AssignDumpId(&rsinfo[j].dobj);
+ snprintf(namebuf, sizeof(namebuf), "row-security of %s",
+ tbinfo->rolname);
+ rsinfo[j].dobj.name = namebuf;
+ rsinfo[j].dobj.namespace = tbinfo->dobj.namespace;
+ rsinfo[j].rstable = tbinfo;
+ rsinfo[j].rseccmd = pg_strdup(PQgetvalue(res, j, i_rseccmd));
+ rsinfo[j].rsecqual = pg_strdup(PQgetvalue(res, j, i_rsecqual));
+ }
+ PQclear(res);
+ }
+ destroyPQExpBuffer(query);
+}
+
+/*
+ * dumpRowSecurity
+ * dump the definition of the given row-security policy
+ */
+static void
+dumpRowSecurity(Archive *fout, RowSecurityInfo *rsinfo)
+{
+ TableInfo *tbinfo = rsinfo->rstable;
+ PQExpBuffer query;
+ PQExpBuffer delqry;
+ const char *cmd;
+
+ if (dataOnly || !tbinfo->hasrowsec)
+ return;
+
+ query = createPQExpBuffer();
+ delqry = createPQExpBuffer();
+ appendPQExpBuffer(query, "ALTER TABLE %s SET ROW SECURITY ",
+ fmtId(tbinfo->dobj.name));
+ appendPQExpBuffer(delqry, "ALTER TABLE %s RESET ROW SECURITY ",
+ fmtId(tbinfo->dobj.name));
+ if (strcmp(rsinfo->rseccmd, "a") == 0)
+ cmd = "ALL";
+ else if (strcmp(rsinfo->rseccmd, "s") == 0)
+ cmd = "SELECT";
+ else if (strcmp(rsinfo->rseccmd, "i") == 0)
+ cmd = "INSERT";
+ else if (strcmp(rsinfo->rseccmd, "u") == 0)
+ cmd = "UPDATE";
+ else if (strcmp(rsinfo->rseccmd, "d") == 0)
+ cmd = "DELETE";
+ else
+ {
+ write_msg(NULL, "unexpected command type: '%s'\n", rsinfo->rseccmd);
+ exit_nicely(1);
+ }
+ appendPQExpBuffer(query, "FOR %s TO %s;\n", cmd, rsinfo->rsecqual);
+ appendPQExpBuffer(delqry, "FOR %s;\n", cmd);
+
+ ArchiveEntry(fout, rsinfo->dobj.catId, rsinfo->dobj.dumpId,
+ rsinfo->dobj.name,
+ rsinfo->dobj.namespace->dobj.name,
+ NULL,
+ tbinfo->rolname, false,
+ "ROW SECURITY", SECTION_POST_DATA,
+ query->data, delqry->data, NULL,
+ NULL, 0,
+ NULL, NULL);
+
+ destroyPQExpBuffer(query);
+}
+
static void
binary_upgrade_set_type_oids_by_type_oid(Archive *fout,
PQExpBuffer upgrade_buffer,
@@ -4242,6 +4371,7 @@ getTables(Archive *fout, int *numTables)
int i_relhastriggers;
int i_relhasindex;
int i_relhasrules;
+ int i_relhasrowsec;
int i_relhasoids;
int i_relfrozenxid;
int i_toastoid;
@@ -4293,10 +4423,7 @@ getTables(Archive *fout, int *numTables)
"(%s c.relowner) AS rolname, "
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
- "c.relfrozenxid, tc.oid AS toid, "
- "tc.relfrozenxid AS tfrozenxid, "
- "c.relpersistence, c.relispopulated, "
- "c.relreplident, c.relpages, "
+ "c.relhasrowsecurity, "
"CASE WHEN c.reloftype <> 0 THEN c.reloftype::pg_catalog.regtype ELSE NULL END AS reloftype, "
"d.refobjid AS owning_tab, "
"d.refobjsubid AS owning_col, "
@@ -4332,6 +4459,7 @@ getTables(Archive *fout, int *numTables)
"(%s c.relowner) AS rolname, "
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"c.relfrozenxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"c.relpersistence, c.relispopulated, "
@@ -4371,6 +4499,7 @@ getTables(Archive *fout, int *numTables)
"(%s c.relowner) AS rolname, "
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"c.relfrozenxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"c.relpersistence, 't' as relispopulated, "
@@ -4408,6 +4537,7 @@ getTables(Archive *fout, int *numTables)
"(%s c.relowner) AS rolname, "
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"c.relfrozenxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"'p' AS relpersistence, 't' as relispopulated, "
@@ -4444,6 +4574,7 @@ getTables(Archive *fout, int *numTables)
"(%s c.relowner) AS rolname, "
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"c.relfrozenxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"'p' AS relpersistence, 't' as relispopulated, "
@@ -4480,6 +4611,7 @@ getTables(Archive *fout, int *numTables)
"(%s c.relowner) AS rolname, "
"c.relchecks, (c.reltriggers <> 0) AS relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"c.relfrozenxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"'p' AS relpersistence, 't' as relispopulated, "
@@ -4516,6 +4648,7 @@ getTables(Archive *fout, int *numTables)
"(%s relowner) AS rolname, "
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"0 AS relfrozenxid, "
"0 AS toid, "
"0 AS tfrozenxid, "
@@ -4552,6 +4685,7 @@ getTables(Archive *fout, int *numTables)
"(%s relowner) AS rolname, "
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"0 AS relfrozenxid, "
"0 AS toid, "
"0 AS tfrozenxid, "
@@ -4584,6 +4718,7 @@ getTables(Archive *fout, int *numTables)
"(%s relowner) AS rolname, "
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"0 AS relfrozenxid, "
"0 AS toid, "
"0 AS tfrozenxid, "
@@ -4611,6 +4746,7 @@ getTables(Archive *fout, int *numTables)
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, "
"'t'::bool AS relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"0 AS relfrozenxid, "
"0 AS toid, "
"0 AS tfrozenxid, "
@@ -4648,6 +4784,7 @@ getTables(Archive *fout, int *numTables)
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, "
"'t'::bool AS relhasoids, "
+ "'f'::bool AS relhasrowsecurity, "
"0 as relfrozenxid, "
"0 AS toid, "
"0 AS tfrozenxid, "
@@ -4695,6 +4832,7 @@ getTables(Archive *fout, int *numTables)
i_relhastriggers = PQfnumber(res, "relhastriggers");
i_relhasindex = PQfnumber(res, "relhasindex");
i_relhasrules = PQfnumber(res, "relhasrules");
+ i_relhasrowsec = PQfnumber(res, "relhasrowsecurity");
i_relhasoids = PQfnumber(res, "relhasoids");
i_relfrozenxid = PQfnumber(res, "relfrozenxid");
i_toastoid = PQfnumber(res, "toid");
@@ -4744,6 +4882,7 @@ getTables(Archive *fout, int *numTables)
tblinfo[i].hasindex = (strcmp(PQgetvalue(res, i, i_relhasindex), "t") == 0);
tblinfo[i].hasrules = (strcmp(PQgetvalue(res, i, i_relhasrules), "t") == 0);
tblinfo[i].hastriggers = (strcmp(PQgetvalue(res, i, i_relhastriggers), "t") == 0);
+ tblinfo[i].hasrowsec = (strcmp(PQgetvalue(res, i, i_relhasrowsec), "t") == 0);
tblinfo[i].hasoids = (strcmp(PQgetvalue(res, i, i_relhasoids), "t") == 0);
tblinfo[i].relispopulated = (strcmp(PQgetvalue(res, i, i_relispopulated), "t") == 0);
tblinfo[i].relreplident = *(PQgetvalue(res, i, i_relreplident));
@@ -7861,6 +8000,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
NULL, 0,
dumpBlobs, NULL);
break;
+ case DO_ROW_SECURITY:
+ dumpRowSecurity(fout, (RowSecurityInfo *) dobj);
+ break;
case DO_PRE_DATA_BOUNDARY:
case DO_POST_DATA_BOUNDARY:
/* never dumped, nothing to do */
@@ -15147,6 +15289,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
case DO_TRIGGER:
case DO_EVENT_TRIGGER:
case DO_DEFAULT_ACL:
+ case DO_ROW_SECURITY:
/* Post-data objects: must come after the post-data boundary */
addObjectDependency(dobj, postDataBound->dumpId);
break;
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index c8dac35..de10c95 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -111,7 +111,8 @@ typedef enum
DO_PRE_DATA_BOUNDARY,
DO_POST_DATA_BOUNDARY,
DO_EVENT_TRIGGER,
- DO_REFRESH_MATVIEW
+ DO_REFRESH_MATVIEW,
+ DO_ROW_SECURITY,
} DumpableObjectType;
typedef struct _dumpableObject
@@ -245,6 +246,7 @@ typedef struct _tableInfo
bool hasindex; /* does it have any indexes? */
bool hasrules; /* does it have any rules? */
bool hastriggers; /* does it have any triggers? */
+ bool hasrowsec; /* does it have any row-security policy? */
bool hasoids; /* does it have OIDs? */
uint32 frozenxid; /* for restore frozen xid */
Oid toast_oid; /* for restore toast frozen xid */
@@ -484,6 +486,14 @@ typedef struct _blobInfo
char *blobacl;
} BlobInfo;
+typedef struct _rowSecurityInfo
+{
+ DumpableObject dobj;
+ TableInfo *rstable;
+ char *rseccmd;
+ char *rsecqual;
+} RowSecurityInfo;
+
/* global decls */
extern bool force_quotes; /* double-quotes for identifiers flag */
extern bool g_verbose; /* verbose flag */
@@ -575,5 +585,6 @@ extern DefaultACLInfo *getDefaultACLs(Archive *fout, int *numDefaultACLs);
extern void getExtensionMembership(Archive *fout, ExtensionInfo extinfo[],
int numExtensions);
extern EventTriggerInfo *getEventTriggers(Archive *fout, int *numEventTriggers);
+extern void getRowSecurity(Archive *fout, TableInfo tblinfo[], int numTables);
#endif /* PG_DUMP_H */
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index bda3535..718007d 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -1342,6 +1342,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
"BLOB DATA (ID %d)",
obj->dumpId);
return;
+ case DO_ROW_SECURITY:
+ snprintf(buf, bufsize,
+ "ROW-SECURITY POLICY (ID %d OID %u)",
+ obj->dumpId, obj->catId.oid);
+ return;
case DO_PRE_DATA_BOUNDARY:
snprintf(buf, bufsize,
"PRE-DATA BOUNDARY (ID %d)",
--
1.8.3.1
0006-RLS-psql-support-for-reporting-row-security-constrai.patchtext/x-patch; name=0006-RLS-psql-support-for-reporting-row-security-constrai.patchDownload
>From db96cb2be9c1d71d7b5f7f3fe21fd636b0e7a9fb Mon Sep 17 00:00:00 2001
From: Craig Ringer <craig@2ndquadrant.com>
Date: Mon, 20 Jan 2014 13:45:24 +0800
Subject: [PATCH 06/10] RLS: psql support for reporting row security
constraints
---
src/bin/psql/describe.c | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 0d4b151..e99b72a 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2764,6 +2764,10 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys
appendPQExpBuffer(&buf,
",\n pg_catalog.obj_description(c.oid, 'pg_class') as \"%s\"",
gettext_noop("Description"));
+ if (pset.sversion >= 90300)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_get_expr(rs.rsecqual, c.oid) as \"%s\"",
+ gettext_noop("Row-security"));
}
appendPQExpBufferStr(&buf,
@@ -2773,6 +2777,9 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys
appendPQExpBufferStr(&buf,
"\n LEFT JOIN pg_catalog.pg_index i ON i.indexrelid = c.oid"
"\n LEFT JOIN pg_catalog.pg_class c2 ON i.indrelid = c2.oid");
+ if (verbose && pset.sversion >= 90300)
+ appendPQExpBuffer(&buf,
+ "\n LEFT JOIN pg_rowsecurity rs ON rs.rsecrelid = c.oid");
appendPQExpBufferStr(&buf, "\nWHERE c.relkind IN (");
if (showTables)
--
1.8.3.1
0007-RLS-Enforce-row-security-by-transforming-query-plans.patchtext/x-patch; name=0007-RLS-Enforce-row-security-by-transforming-query-plans.patchDownload
>From a88e62608515b82eccc78d005e4f838ceb8b51e3 Mon Sep 17 00:00:00 2001
From: Craig Ringer <craig@2ndquadrant.com>
Date: Mon, 20 Jan 2014 13:49:56 +0800
Subject: [PATCH 07/10] RLS: Enforce row-security by transforming query plans
---
src/backend/executor/execMain.c | 27 +-
src/backend/nodes/copyfuncs.c | 2 +
src/backend/nodes/equalfuncs.c | 2 +
src/backend/nodes/nodeFuncs.c | 12 +-
src/backend/nodes/outfuncs.c | 2 +
src/backend/nodes/readfuncs.c | 1 +
src/backend/optimizer/plan/planner.c | 21 +-
src/backend/optimizer/prep/preptlist.c | 68 ++-
src/backend/optimizer/prep/prepunion.c | 95 +++-
src/backend/optimizer/util/Makefile | 3 +-
src/backend/optimizer/util/rowsecurity.c | 744 +++++++++++++++++++++++++++++++
src/backend/rewrite/rewriteHandler.c | 16 +
src/backend/utils/adt/ri_triggers.c | 13 +-
src/include/miscadmin.h | 1 +
src/include/nodes/nodeFuncs.h | 1 +
src/include/nodes/parsenodes.h | 4 +-
src/include/nodes/relation.h | 4 +
src/include/optimizer/rowsecurity.h | 27 ++
src/include/rewrite/rewriteHandler.h | 1 +
19 files changed, 1016 insertions(+), 28 deletions(-)
create mode 100644 src/backend/optimizer/util/rowsecurity.c
create mode 100644 src/include/optimizer/rowsecurity.h
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 6b5f198..0041c23 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -789,8 +789,9 @@ InitPlan(QueryDesc *queryDesc, int eflags)
foreach(l, plannedstmt->rowMarks)
{
PlanRowMark *rc = (PlanRowMark *) lfirst(l);
- Oid relid;
- Relation relation;
+ RangeTblEntry *rte = NULL;
+ Relation relation = NULL;
+ LOCKMODE lockmode = NoLock;
ExecRowMark *erm;
/* ignore "parent" rowmarks; they are irrelevant at runtime */
@@ -803,27 +804,33 @@ InitPlan(QueryDesc *queryDesc, int eflags)
case ROW_MARK_NOKEYEXCLUSIVE:
case ROW_MARK_SHARE:
case ROW_MARK_KEYSHARE:
- relid = getrelid(rc->rti, rangeTable);
- relation = heap_open(relid, RowShareLock);
+ rte = rt_fetch(rc->rti, rangeTable);
+ lockmode = RowShareLock;
break;
case ROW_MARK_REFERENCE:
- relid = getrelid(rc->rti, rangeTable);
- relation = heap_open(relid, AccessShareLock);
+ rte = rt_fetch(rc->rti, rangeTable);
+ lockmode = AccessShareLock;
break;
case ROW_MARK_COPY:
/* there's no real table here ... */
- relation = NULL;
break;
default:
elog(ERROR, "unrecognized markType: %d", rc->markType);
- relation = NULL; /* keep compiler quiet */
break;
}
/* Check that relation is a legal target for marking */
- if (relation)
+ if (rte)
+ {
+ if (rte->rtekind == RTE_RELATION)
+ relation = heap_open(rte->relid, lockmode);
+ else
+ {
+ Assert(rte->rtekind == RTE_SUBQUERY);
+ relation = heap_open(rte->rowsec_relid, lockmode);
+ }
CheckValidRowMarkRel(relation, rc->markType);
-
+ }
erm = (ExecRowMark *) palloc(sizeof(ExecRowMark));
erm->relation = relation;
erm->rti = rc->rti;
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 52bd8f5..3a8b60c 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -1936,6 +1936,7 @@ _copyAppendRelInfo(const AppendRelInfo *from)
COPY_SCALAR_FIELD(parent_relid);
COPY_SCALAR_FIELD(child_relid);
+ COPY_SCALAR_FIELD(child_result);
COPY_SCALAR_FIELD(parent_reltype);
COPY_SCALAR_FIELD(child_reltype);
COPY_NODE_FIELD(translated_vars);
@@ -2467,6 +2468,7 @@ _copyQuery(const Query *from)
COPY_SCALAR_FIELD(canSetTag);
COPY_NODE_FIELD(utilityStmt);
COPY_SCALAR_FIELD(resultRelation);
+ COPY_SCALAR_FIELD(sourceRelation);
COPY_SCALAR_FIELD(hasAggs);
COPY_SCALAR_FIELD(hasWindowFuncs);
COPY_SCALAR_FIELD(hasSubLinks);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index b9b0de2..cb2abf6 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -814,6 +814,7 @@ _equalAppendRelInfo(const AppendRelInfo *a, const AppendRelInfo *b)
{
COMPARE_SCALAR_FIELD(parent_relid);
COMPARE_SCALAR_FIELD(child_relid);
+ COMPARE_SCALAR_FIELD(child_result);
COMPARE_SCALAR_FIELD(parent_reltype);
COMPARE_SCALAR_FIELD(child_reltype);
COMPARE_NODE_FIELD(translated_vars);
@@ -849,6 +850,7 @@ _equalQuery(const Query *a, const Query *b)
COMPARE_SCALAR_FIELD(canSetTag);
COMPARE_NODE_FIELD(utilityStmt);
COMPARE_SCALAR_FIELD(resultRelation);
+ COMPARE_SCALAR_FIELD(sourceRelation);
COMPARE_SCALAR_FIELD(hasAggs);
COMPARE_SCALAR_FIELD(hasWindowFuncs);
COMPARE_SCALAR_FIELD(hasSubLinks);
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 123f2a6..fa8f040 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -1948,8 +1948,11 @@ query_tree_walker(Query *query,
return true;
if (walker((Node *) query->withCheckOptions, context))
return true;
- if (walker((Node *) query->returningList, context))
- return true;
+ if (!(flags & QTW_IGNORE_RETURNING))
+ {
+ if (walker((Node *) query->returningList, context))
+ return true;
+ }
if (walker((Node *) query->jointree, context))
return true;
if (walker(query->setOperations, context))
@@ -2686,7 +2689,10 @@ query_tree_mutator(Query *query,
MUTATE(query->targetList, query->targetList, List *);
MUTATE(query->withCheckOptions, query->withCheckOptions, List *);
- MUTATE(query->returningList, query->returningList, List *);
+ if (!(flags & QTW_IGNORE_RETURNING))
+ MUTATE(query->returningList, query->returningList, List *);
+ else
+ query->returningList = copyObject(query->returningList);
MUTATE(query->jointree, query->jointree, FromExpr *);
MUTATE(query->setOperations, query->setOperations, Node *);
MUTATE(query->havingQual, query->havingQual, Node *);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index cfa4cb3..500faeb 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -1925,6 +1925,7 @@ _outAppendRelInfo(StringInfo str, const AppendRelInfo *node)
WRITE_UINT_FIELD(parent_relid);
WRITE_UINT_FIELD(child_relid);
+ WRITE_UINT_FIELD(child_result);
WRITE_OID_FIELD(parent_reltype);
WRITE_OID_FIELD(child_reltype);
WRITE_NODE_FIELD(translated_vars);
@@ -2241,6 +2242,7 @@ _outQuery(StringInfo str, const Query *node)
appendStringInfoString(str, " :utilityStmt <>");
WRITE_INT_FIELD(resultRelation);
+ WRITE_INT_FIELD(sourceRelation);
WRITE_BOOL_FIELD(hasAggs);
WRITE_BOOL_FIELD(hasWindowFuncs);
WRITE_BOOL_FIELD(hasSubLinks);
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 335579f..884f45c 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -201,6 +201,7 @@ _readQuery(void)
READ_BOOL_FIELD(canSetTag);
READ_NODE_FIELD(utilityStmt);
READ_INT_FIELD(resultRelation);
+ READ_INT_FIELD(sourceRelation);
READ_BOOL_FIELD(hasAggs);
READ_BOOL_FIELD(hasWindowFuncs);
READ_BOOL_FIELD(hasSubLinks);
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 8c12eb9..63f0ec2 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -33,6 +33,7 @@
#include "optimizer/planmain.h"
#include "optimizer/planner.h"
#include "optimizer/prep.h"
+#include "optimizer/rowsecurity.h"
#include "optimizer/subselect.h"
#include "optimizer/tlist.h"
#include "parser/analyze.h"
@@ -406,6 +407,19 @@ subquery_planner(PlannerGlobal *glob, Query *parse,
expand_inherited_tables(root);
/*
+ * Apply row-security policy of the relation being referenced,
+ * if configured with either of built-in or extension's features.
+ * RangeTblEntry of the relation with row-security policy shall
+ * be replaced with a row-security subquery that has simple scan
+ * on the target relation with row-security policy qualifiers.
+ *
+ * This routine assumes PlannerInfo is already handled with
+ * expand_inherited_tables, thus, AppendRelInfo or PlanRowMark
+ * have valid information.
+ */
+ apply_row_security_policy(root);
+
+ /*
* Set hasHavingQual to remember if HAVING clause is present. Needed
* because preprocess_expression will reduce a constant-true condition to
* an empty qual list ... but "HAVING TRUE" is not a semantic no-op.
@@ -898,6 +912,8 @@ inheritance_planner(PlannerInfo *root)
ChangeVarNodes((Node *) subroot.parse, rti, newrti, 0);
ChangeVarNodes((Node *) subroot.rowMarks, rti, newrti, 0);
ChangeVarNodes((Node *) subroot.append_rel_list, rti, newrti, 0);
+ if (subroot.parse->sourceRelation == rti)
+ subroot.parse->sourceRelation = newrti;
rte = copyObject(rte);
subroot.parse->rtable = lappend(subroot.parse->rtable,
rte);
@@ -960,7 +976,10 @@ inheritance_planner(PlannerInfo *root)
root->init_plans = subroot.init_plans;
/* Build list of target-relation RT indexes */
- resultRelations = lappend_int(resultRelations, appinfo->child_relid);
+ resultRelations = lappend_int(resultRelations,
+ (appinfo->child_result > 0 ?
+ appinfo->child_result :
+ appinfo->child_relid));
/* Build lists of per-relation WCO and RETURNING targetlists */
if (parse->withCheckOptions)
diff --git a/src/backend/optimizer/prep/preptlist.c b/src/backend/optimizer/prep/preptlist.c
index ee773b8..5e4a010 100644
--- a/src/backend/optimizer/prep/preptlist.c
+++ b/src/backend/optimizer/prep/preptlist.c
@@ -37,8 +37,49 @@
static List *expand_targetlist(List *tlist, int command_type,
- Index result_relation, List *range_table);
+ Index result_relation, Index source_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 Var that references sub-queries being originated from regular
+ * relations with row-level security policy due to nature of sub-query
+ * that has no system-column.
+ */
+static AttrNumber
+lookup_varattno(AttrNumber attno, Index rt_index, List *rtables)
+{
+ RangeTblEntry *rte = rt_fetch(rt_index, rtables);
+
+ if (rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_SECURITY)
+ {
+ ListCell *cell;
+
+ foreach (cell, rte->subquery->targetList)
+ {
+ TargetEntry *tle = lfirst(cell);
+ Var *var;
+
+ if (IsA(tle->expr, Const))
+ continue;
+
+ var = (Var *) tle->expr;
+ Assert(IsA(var, Var));
+
+ if (var->varattno == attno)
+ return tle->resno;
+ }
+ elog(ERROR, "invalid attno %d on row-security subquery target-list",
+ attno);
+ }
+ return attno;
+}
/*
* preprocess_targetlist
@@ -51,6 +92,7 @@ preprocess_targetlist(PlannerInfo *root, List *tlist)
{
Query *parse = root->parse;
int result_relation = parse->resultRelation;
+ int source_relation = parse->sourceRelation;
List *range_table = parse->rtable;
CmdType command_type = parse->commandType;
ListCell *lc;
@@ -73,8 +115,12 @@ preprocess_targetlist(PlannerInfo *root, List *tlist)
* 10/94
*/
if (command_type == CMD_INSERT || command_type == CMD_UPDATE)
+ {
tlist = expand_targetlist(tlist, command_type,
- result_relation, range_table);
+ result_relation,
+ source_relation,
+ range_table);
+ }
/*
* Add necessary junk columns for rowmarked rels. These values are needed
@@ -96,7 +142,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,
@@ -112,7 +159,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,
@@ -195,7 +243,8 @@ preprocess_targetlist(PlannerInfo *root, List *tlist)
*/
static List *
expand_targetlist(List *tlist, int command_type,
- Index result_relation, List *range_table)
+ Index result_relation, Index source_relation,
+ List *range_table)
{
List *new_tlist = NIL;
ListCell *tlist_item;
@@ -218,6 +267,9 @@ expand_targetlist(List *tlist, int command_type,
numattrs = RelationGetNumberOfAttributes(rel);
+ if (source_relation == 0)
+ source_relation = result_relation;
+
for (attrno = 1; attrno <= numattrs; attrno++)
{
Form_pg_attribute att_tup = rel->rd_att->attrs[attrno - 1];
@@ -298,8 +350,10 @@ expand_targetlist(List *tlist, int command_type,
case CMD_UPDATE:
if (!att_tup->attisdropped)
{
- new_expr = (Node *) makeVar(result_relation,
- attrno,
+ new_expr = (Node *) makeVar(source_relation,
+ lookup_varattno(attrno,
+ source_relation,
+ range_table),
atttype,
atttypmod,
attcollation,
diff --git a/src/backend/optimizer/prep/prepunion.c b/src/backend/optimizer/prep/prepunion.c
index 52dcc72..9bd97e7 100644
--- a/src/backend/optimizer/prep/prepunion.c
+++ b/src/backend/optimizer/prep/prepunion.c
@@ -55,6 +55,7 @@ typedef struct
{
PlannerInfo *root;
AppendRelInfo *appinfo;
+ bool in_returning;
} adjust_appendrel_attrs_context;
static Plan *recurse_set_operations(Node *setOp, PlannerInfo *root,
@@ -1594,6 +1595,7 @@ adjust_appendrel_attrs(PlannerInfo *root, Node *node, AppendRelInfo *appinfo)
context.root = root;
context.appinfo = appinfo;
+ context.in_returning = false;
/*
* Must be prepared to start with a Query or a bare expression tree.
@@ -1605,10 +1607,29 @@ adjust_appendrel_attrs(PlannerInfo *root, Node *node, AppendRelInfo *appinfo)
newnode = query_tree_mutator((Query *) node,
adjust_appendrel_attrs_mutator,
(void *) &context,
- QTW_IGNORE_RC_SUBQUERIES);
+ QTW_IGNORE_RC_SUBQUERIES |
+ QTW_IGNORE_RETURNING);
+ /*
+ * Returning clause on the relation being replaced with row-
+ * security subquery shall be handled in a special way, because
+ * of no system columns on subquery.
+ * Var references to system column or whole-row reference need
+ * to be adjusted to reference pseudo columns on behalf of
+ * the underlying these columns, however, RETURNGIN clause is
+ * an exception because its Var nodes are evaluated towards
+ * the "raw" target relation, not a fetched tuple.
+ */
+ context.in_returning = true;
+ newnode->returningList = (List *)
+ expression_tree_mutator((Node *) newnode->returningList,
+ adjust_appendrel_attrs_mutator,
+ (void *) &context);
if (newnode->resultRelation == appinfo->parent_relid)
{
- newnode->resultRelation = appinfo->child_relid;
+ newnode->resultRelation = (appinfo->child_result > 0 ?
+ appinfo->child_result :
+ appinfo->child_relid);
+ newnode->sourceRelation = appinfo->child_relid;
/* Fix tlist resnos too, if it's inherited UPDATE */
if (newnode->commandType == CMD_UPDATE)
newnode->targetList =
@@ -1624,6 +1645,49 @@ adjust_appendrel_attrs(PlannerInfo *root, Node *node, AppendRelInfo *appinfo)
}
static Node *
+fixup_var_on_rowsec_subquery(RangeTblEntry *rte, Var *var)
+{
+ ListCell *cell;
+
+ Assert(rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_SECURITY);
+ /*
+ * In case when row-level security policy is applied on the referenced
+ * table, its RangeTblEntry (RTE_RELATION) is replaced with sub-query
+ * to filter out unprivileged rows of underlying relation.
+ * Even though reference to this sub-query should perform as if ones
+ * to real relations, system column has to be cared in special way
+ * due to the nature of sub-query.
+ * Target-entries that reference system columns should be added on
+ * rowlevelsec.c, so all we need to do here is looking up underlying
+ * target-list that can reference underlying system column, and fix-
+ * up varattno of the referencing Var node with resno of TargetEntry.
+ */
+ foreach (cell, rte->subquery->targetList)
+ {
+ TargetEntry *subtle = lfirst(cell);
+
+ if (IsA(subtle->expr, Var))
+ {
+ Var *subvar = (Var *) subtle->expr;
+ Var *newnode;
+
+ if (subvar->varattno == var->varattno)
+ {
+ newnode = copyObject(var);
+ newnode->varattno = subtle->resno;
+ return (Node *)newnode;
+ }
+ }
+ else
+ Assert(IsA(subtle->expr, Const));
+ }
+ elog(ERROR, "could not find pseudo column of %d, relid %u",
+ var->varattno, var->varno);
+ return NULL;
+}
+
+static Node *
adjust_appendrel_attrs_mutator(Node *node,
adjust_appendrel_attrs_context *context)
{
@@ -1638,8 +1702,12 @@ adjust_appendrel_attrs_mutator(Node *node,
if (var->varlevelsup == 0 &&
var->varno == appinfo->parent_relid)
{
- var->varno = appinfo->child_relid;
+ var->varno = (context->in_returning &&
+ appinfo->child_result > 0 ?
+ appinfo->child_result :
+ appinfo->child_relid);
var->varnoold = appinfo->child_relid;
+
if (var->varattno > 0)
{
Node *newnode;
@@ -1664,6 +1732,14 @@ adjust_appendrel_attrs_mutator(Node *node,
*/
if (OidIsValid(appinfo->child_reltype))
{
+ Query *parse = context->root->parse;
+ RangeTblEntry *rte = rt_fetch(appinfo->child_relid,
+ parse->rtable);
+ if (!context->in_returning &&
+ rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_SECURITY)
+ var = (Var *)fixup_var_on_rowsec_subquery(rte, var);
+
Assert(var->vartype == appinfo->parent_reltype);
if (appinfo->parent_reltype != appinfo->child_reltype)
{
@@ -1708,7 +1784,18 @@ adjust_appendrel_attrs_mutator(Node *node,
return (Node *) rowexpr;
}
}
- /* system attributes don't need any other translation */
+ else
+ {
+ Query *parse = context->root->parse;
+ RangeTblEntry *rte;
+
+ rte = rt_fetch(appinfo->child_relid, parse->rtable);
+
+ if (!context->in_returning &&
+ rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_SECURITY)
+ return fixup_var_on_rowsec_subquery(rte, var);
+ }
}
return (Node *) var;
}
diff --git a/src/backend/optimizer/util/Makefile b/src/backend/optimizer/util/Makefile
index c54d0a6..d42a285 100644
--- a/src/backend/optimizer/util/Makefile
+++ b/src/backend/optimizer/util/Makefile
@@ -13,6 +13,7 @@ top_builddir = ../../../..
include $(top_builddir)/src/Makefile.global
OBJS = clauses.o joininfo.o orclauses.o pathnode.o placeholder.o \
- plancat.o predtest.o relnode.o restrictinfo.o tlist.o var.o
+ plancat.o predtest.o relnode.o restrictinfo.o tlist.o var.o \
+ rowsecurity.o
include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/optimizer/util/rowsecurity.c b/src/backend/optimizer/util/rowsecurity.c
new file mode 100644
index 0000000..c2e5a49
--- /dev/null
+++ b/src/backend/optimizer/util/rowsecurity.c
@@ -0,0 +1,744 @@
+/*
+ * optimizer/util/rowsecurity.c
+ * Routines to support row-security feature
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ */
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "access/htup_details.h"
+#include "access/sysattr.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_inherits_fn.h"
+#include "catalog/pg_rowsecurity.h"
+#include "catalog/pg_type.h"
+#include "miscadmin.h"
+#include "nodes/makefuncs.h"
+#include "nodes/nodeFuncs.h"
+#include "nodes/plannodes.h"
+#include "optimizer/clauses.h"
+#include "optimizer/prep.h"
+#include "optimizer/rowsecurity.h"
+#include "parser/parsetree.h"
+#include "rewrite/rewriteHandler.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "tcop/utility.h"
+
+/* flags to pull row-security policy */
+#define RSEC_FLAG_HAS_SUBLINKS 0x0001
+
+/* hook to allow extensions to apply their own security policy */
+row_security_policy_hook_type row_security_policy_hook = NULL;
+
+/*
+ * make_pseudo_column
+ *
+ * It makes a target-entry node that references underlying column.
+ * Its tle->expr is usualy Var node, but may be Const for dummy NULL
+ * if the supplied attribute was already dropped.
+ */
+static TargetEntry *
+make_pseudo_column(RangeTblEntry *subrte, AttrNumber attnum)
+{
+ Expr *expr;
+ char *resname;
+
+ Assert(subrte->rtekind == RTE_RELATION && OidIsValid(subrte->relid));
+ if (attnum == InvalidAttrNumber)
+ {
+ expr = (Expr *) makeWholeRowVar(subrte, (Index) 1, 0, false);
+ resname = get_rel_name(subrte->relid);
+ }
+ else
+ {
+ HeapTuple tuple;
+ Form_pg_attribute attform;
+
+ tuple = SearchSysCache2(ATTNUM,
+ ObjectIdGetDatum(subrte->relid),
+ Int16GetDatum(attnum));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for attribute %d of relation %u",
+ attnum, subrte->relid);
+ attform = (Form_pg_attribute) GETSTRUCT(tuple);
+
+ if (attform->attisdropped)
+ {
+ char namebuf[NAMEDATALEN];
+
+ /* Insert NULL just for a placeholder of dropped column */
+ expr = (Expr *) makeConst(INT4OID,
+ -1,
+ InvalidOid,
+ sizeof(int32),
+ (Datum) 0,
+ true, /* isnull */
+ true); /* byval */
+ sprintf(namebuf, "dummy-%d", (int)attform->attnum);
+ resname = pstrdup(namebuf);
+ }
+ else
+ {
+ expr = (Expr *) makeVar((Index) 1,
+ attform->attnum,
+ attform->atttypid,
+ attform->atttypmod,
+ attform->attcollation,
+ 0);
+ resname = pstrdup(NameStr(attform->attname));
+ }
+ ReleaseSysCache(tuple);
+ }
+ return makeTargetEntry(expr, -1, resname, false);
+}
+
+/*
+ * lookup_pseudo_column
+ *
+ * It looks-up resource number of the target-entry relevant to the given
+ * Var-node that references the row-security subquery. If required column
+ * is not in the subquery's target-list, this function also adds new one
+ * and returns its resource number.
+ */
+static AttrNumber
+lookup_pseudo_column(PlannerInfo *root,
+ RangeTblEntry *rte, AttrNumber varattno)
+{
+ Query *subqry;
+ RangeTblEntry *subrte;
+ TargetEntry *subtle;
+ ListCell *cell;
+
+ Assert(rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_SECURITY);
+
+ subqry = rte->subquery;
+ foreach (cell, subqry->targetList)
+ {
+ subtle = lfirst(cell);
+
+ /*
+ * If referenced artifical column is already constructed on the
+ * target-list of row-security subquery, nothing to do any more.
+ */
+ if (IsA(subtle->expr, Var))
+ {
+ Var *subvar = (Var *)subtle->expr;
+
+ Assert(subvar->varno == 1);
+ if (subvar->varattno == varattno)
+ return subtle->resno;
+ }
+ }
+
+ /*
+ * OK, we don't have an artifical column relevant to the required ones,
+ * so let's create a new artifical column on demand.
+ */
+ subrte = rt_fetch((Index) 1, subqry->rtable);
+ subtle = make_pseudo_column(subrte, varattno);
+ subtle->resno = list_length(subqry->targetList) + 1;
+
+ subqry->targetList = lappend(subqry->targetList, subtle);
+ rte->eref->colnames = lappend(rte->eref->colnames,
+ makeString(pstrdup(subtle->resname)));
+ return subtle->resno;
+}
+
+/*
+ * fixup_varnode_walker
+ *
+ * It recursively fixes up references to the relation to be replaced by
+ * row-security sub-query, and adds pseudo columns relevant to the
+ * underlying system columns or whole row-reference on demand.
+ */
+typedef struct {
+ PlannerInfo *root;
+ int varlevelsup;
+ Index *vartrans;
+} fixup_varnode_context;
+
+static bool
+fixup_varnode_walker(Node *node, fixup_varnode_context *context)
+{
+ if (node == NULL)
+ return false;
+
+ if (IsA(node, Var))
+ {
+ Var *var = (Var *) node;
+ List *rtable = context->root->parse->rtable;
+ RangeTblEntry *rte;
+ ListCell *cell;
+
+ /*
+ * Ignore it, if Var node does not reference the Query currently
+ * we focus on.
+ */
+ if (var->varlevelsup != context->varlevelsup)
+ return false;
+
+ if (context->vartrans[var->varno] > 0)
+ {
+ Index rtindex_trans = context->vartrans[var->varno];
+
+ rte = rt_fetch(rtindex_trans, rtable);
+ Assert(rte->rtekind == RTE_SUBQUERY &&
+ rte->subquery->querySource == QSRC_ROW_SECURITY);
+
+ var->varno = var->varnoold = rtindex_trans;
+ var->varattno = lookup_pseudo_column(context->root, rte,
+ var->varattno);
+ }
+ else
+ {
+ rte = rt_fetch(var->varno, rtable);
+ if (rte->rtekind == RTE_RELATION && rte->inh)
+ {
+ foreach (cell, context->root->append_rel_list)
+ {
+ AppendRelInfo *appinfo = lfirst(cell);
+ RangeTblEntry *child_rte;
+
+ if (appinfo->parent_relid != var->varno)
+ continue;
+
+ child_rte = rt_fetch(appinfo->child_relid, rtable);
+ if (child_rte->rtekind == RTE_SUBQUERY &&
+ child_rte->subquery->querySource == QSRC_ROW_SECURITY)
+ (void) lookup_pseudo_column(context->root,
+ child_rte,
+ var->varattno);
+ }
+ }
+ }
+ }
+ else if (IsA(node, RangeTblRef))
+ {
+ RangeTblRef *rtr = (RangeTblRef *) node;
+
+ if (context->varlevelsup == 0 &&
+ context->vartrans[rtr->rtindex] != 0)
+ rtr->rtindex = context->vartrans[rtr->rtindex];
+ }
+ else if (IsA(node, Query))
+ {
+ bool result;
+
+ context->varlevelsup++;
+ result = query_tree_walker((Query *) node,
+ fixup_varnode_walker,
+ (void *) context, 0);
+ context->varlevelsup--;
+
+ return result;
+ }
+ return expression_tree_walker(node,
+ fixup_varnode_walker,
+ (void *) context);
+}
+
+/*
+ * check_infinite_recursion
+ *
+ * It is a wrong row-security configuration, if we try to expand
+ * the relation inside of row-security subquery originated from
+ * same relation!
+ */
+static void
+check_infinite_recursion(PlannerInfo *root, Oid relid)
+{
+ PlannerInfo *parent = root->parent_root;
+
+ if (parent && parent->parse->querySource == QSRC_ROW_SECURITY)
+ {
+ RangeTblEntry *rte = rt_fetch(1, parent->parse->rtable);
+
+ Assert(rte->rtekind == RTE_RELATION && OidIsValid(rte->relid));
+
+ if (relid == rte->relid)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("infinite recursion detected for relation \"%s\"",
+ get_rel_name(relid))));
+ check_infinite_recursion(parent, relid);
+ }
+}
+
+/*
+ * expand_rtentry_with_policy
+ *
+ * It extends a range-table entry of row-security sub-query with supplied
+ * security policy, and append it on the parse->rtable.
+ * This sub-query contains pseudo columns that reference underlying
+ * regular columns (at least, references to system column or whole of
+ * table reference shall be added on demand), and simple scan on the
+ * target relation.
+ * Any Var nodes that referenced the relation pointed by rtindex shall
+ * be adjusted to reference this sub-query instead. walker
+ */
+static Index
+expand_rtentry_with_policy(PlannerInfo *root, Index rtindex,
+ Expr *qual, int flags)
+{
+ Query *parse = root->parse;
+ RangeTblEntry *rte = rt_fetch(rtindex, parse->rtable);
+ Query *subqry;
+ RangeTblEntry *subrte;
+ RangeTblRef *subrtr;
+ TargetEntry *subtle;
+ RangeTblEntry *newrte;
+ HeapTuple tuple;
+ AttrNumber nattrs;
+ AttrNumber attnum;
+ List *targetList = NIL;
+ List *colNameList = NIL;
+ PlanRowMark *rowmark;
+
+ Assert(rte->rtekind == RTE_RELATION && !rte->inh);
+
+ /* check recursion to prevent infinite loop */
+ check_infinite_recursion(root, rte->relid);
+
+ /* Expand views inside SubLink node */
+ if (flags & RSEC_FLAG_HAS_SUBLINKS)
+ QueryRewriteExpr((Node *)qual, list_make1_oid(rte->relid));
+
+ /*
+ * Construction of sub-query
+ */
+ subqry = (Query *) makeNode(Query);
+ subqry->commandType = CMD_SELECT;
+ subqry->querySource = QSRC_ROW_SECURITY;
+
+ subrte = copyObject(rte);
+ subqry->rtable = list_make1(subrte);
+
+ subrtr = makeNode(RangeTblRef);
+ subrtr->rtindex = 1;
+ subqry->jointree = makeFromExpr(list_make1(subrtr), (Node *) qual);
+ if (flags & RSEC_FLAG_HAS_SUBLINKS)
+ subqry->hasSubLinks = true;
+
+ /*
+ * Construction of TargetEntries that reference underlying columns.
+ */
+ tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(rte->relid));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for relation %u", rte->relid);
+ nattrs = ((Form_pg_class) GETSTRUCT(tuple))->relnatts;
+ ReleaseSysCache(tuple);
+
+ for (attnum = 1; attnum <= nattrs; attnum++)
+ {
+ subtle = make_pseudo_column(subrte, attnum);
+ subtle->resno = list_length(targetList) + 1;
+ Assert(subtle->resno == attnum);
+
+ targetList = lappend(targetList, subtle);
+ colNameList = lappend(colNameList,
+ makeString(pstrdup(subtle->resname)));
+ }
+ subqry->targetList = targetList;
+
+ /* Expand RengeTblEntry with this sub-query */
+ newrte = makeNode(RangeTblEntry);
+ newrte->rtekind = RTE_SUBQUERY;
+ newrte->subquery = subqry;
+ newrte->security_barrier = true;
+ newrte->rowsec_relid = rte->relid;
+ newrte->eref = makeAlias(get_rel_name(rte->relid), colNameList);
+
+ parse->rtable = lappend(parse->rtable, newrte);
+
+ /*
+ * Fix up PlanRowMark if needed, then add references to 'tableoid' and
+ * 'ctid' that shall be added to handle row-level locking.
+ * Also see preprocess_targetlist() that adds some junk attributes.
+ */
+ rowmark = get_plan_rowmark(root->rowMarks, rtindex);
+ if (rowmark)
+ {
+ if (rowmark->rti == rowmark->prti)
+ rowmark->rti = rowmark->prti = list_length(parse->rtable);
+ else
+ rowmark->rti = list_length(parse->rtable);
+
+ lookup_pseudo_column(root, newrte, SelfItemPointerAttributeNumber);
+ lookup_pseudo_column(root, newrte, TableOidAttributeNumber);
+ }
+ return list_length(parse->rtable);
+}
+
+/*
+ * pull_row_security_policy
+ *
+ * It pulls the configured row-security policy of both built-in and
+ * extensions. If any, it returns expression tree.
+ */
+static Expr *
+pull_row_security_policy(CmdType cmd, Relation relation, int *p_flags)
+{
+ Expr *quals = NULL;
+ int flags = 0;
+
+ /*
+ * Pull the row-security policy configured with built-in features,
+ * if unprivileged users. Please note that superuser can bypass it.
+ */
+ if (relation->rsdesc && !superuser())
+ {
+ RowSecurityDesc *rsdesc = relation->rsdesc;
+
+ quals = copyObject(rsdesc->rsall.qual);
+ if (rsdesc->rsall.hassublinks)
+ flags |= RSEC_FLAG_HAS_SUBLINKS;
+ }
+
+ /*
+ * Also, ask extensions whether they want to apply their own
+ * row-security policy. If both built-in and extension has
+ * their own policy, it shall be merged.
+ */
+ if (row_security_policy_hook)
+ {
+ List *temp;
+
+ temp = (*row_security_policy_hook)(cmd, relation);
+ if (temp != NIL)
+ {
+ if ((flags & RSEC_FLAG_HAS_SUBLINKS) == 0 &&
+ contain_subplans((Node *) temp))
+ flags |= RSEC_FLAG_HAS_SUBLINKS;
+
+ if (quals != NULL)
+ temp = lappend(temp, quals);
+
+ if (list_length(temp) == 1)
+ quals = (Expr *)list_head(temp);
+ else if (list_length(temp) > 1)
+ quals = makeBoolExpr(AND_EXPR, temp, -1);
+ }
+ }
+ *p_flags = flags;
+ return quals;
+}
+
+/*
+ * copy_row_security_policy
+ *
+ * It construct a row-security subquery instead of raw COPY TO statement,
+ * if target relation has a row-level security policy
+ */
+bool
+copy_row_security_policy(CopyStmt *stmt, Relation rel, List *attnums)
+{
+ Expr *quals;
+ int flags;
+ Query *parse;
+ RangeTblEntry *rte;
+ RangeTblRef *rtr;
+ TargetEntry *tle;
+ Var *var;
+ ListCell *cell;
+
+ if (stmt->is_from)
+ return false;
+
+ quals = pull_row_security_policy(CMD_SELECT, rel, &flags);
+ if (!quals)
+ return false;
+
+ parse = (Query *) makeNode(Query);
+ parse->commandType = CMD_SELECT;
+ parse->querySource = QSRC_ROW_SECURITY;
+
+ rte = makeNode(RangeTblEntry);
+ rte->rtekind = RTE_RELATION;
+ rte->relid = RelationGetRelid(rel);
+ rte->relkind = RelationGetForm(rel)->relkind;
+
+ foreach (cell, attnums)
+ {
+ HeapTuple tuple;
+ Form_pg_attribute attform;
+ AttrNumber attno = lfirst_int(cell);
+
+ tuple = SearchSysCache2(ATTNUM,
+ ObjectIdGetDatum(RelationGetRelid(rel)),
+ Int16GetDatum(attno));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for attribute %d of relation %s",
+ attno, RelationGetRelationName(rel));
+ attform = (Form_pg_attribute) GETSTRUCT(tuple);
+
+ var = makeVar((Index) 1,
+ attform->attnum,
+ attform->atttypid,
+ attform->atttypmod,
+ attform->attcollation,
+ 0);
+ tle = makeTargetEntry((Expr *) var,
+ list_length(parse->targetList) + 1,
+ pstrdup(NameStr(attform->attname)),
+ false);
+ parse->targetList = lappend(parse->targetList, tle);
+
+ ReleaseSysCache(tuple);
+
+ rte->selectedCols = bms_add_member(rte->selectedCols,
+ attno - FirstLowInvalidHeapAttributeNumber);
+ }
+ rte->inFromCl = true;
+ rte->requiredPerms = ACL_SELECT;
+
+ rtr = makeNode(RangeTblRef);
+ rtr->rtindex = 1;
+
+ parse->jointree = makeFromExpr(list_make1(rtr), (Node *) quals);
+ parse->rtable = list_make1(rte);
+ if (flags & RSEC_FLAG_HAS_SUBLINKS)
+ parse->hasSubLinks = true;
+
+ stmt->query = (Node *) parse;
+
+ return true;
+}
+
+/*
+ * apply_row_security_relation
+ *
+ * It applies row-security policy on a particular relation being specified.
+ * If this relation is top of the inheritance tree, it also checks inherited
+ * children.
+ */
+static bool
+apply_row_security_relation(PlannerInfo *root, Index *vartrans,
+ CmdType cmd, Index rtindex)
+{
+ Query *parse = root->parse;
+ RangeTblEntry *rte = rt_fetch(rtindex, parse->rtable);
+ Relation rel;
+ Expr *qual;
+ int flags;
+ bool result = false;
+
+ if (!rte->inh)
+ {
+ rel = heap_open(rte->relid, NoLock);
+ qual = pull_row_security_policy(cmd, rel, &flags);
+ if (qual)
+ {
+ vartrans[rtindex]
+ = expand_rtentry_with_policy(root, rtindex, qual, flags);
+ if (parse->resultRelation == rtindex)
+ parse->sourceRelation = vartrans[rtindex];
+ result = true;
+ }
+ heap_close(rel, NoLock);
+ }
+ else
+ {
+ /*
+ * In case when relation has inherited children, we try to apply
+ * row-level security policy of them if configured.
+ * In addition to regular replacement with a sub-query, we need
+ * to adjust rtindex of AppendRelInfo and varno of translated_vars.
+ * It makes sub-queries perform like regular relations being
+ * inherited from a particular parent relation. So, a table scan
+ * may have underlying a relation scan and two sub-query scans for
+ * instance. If it is result relation of UPDATE or DELETE command,
+ * rtindex to the original relation (regular relation) has to be
+ * kept because sub-query cannot perform as an updatable relation.
+ * So, we save it on child_result of AppendRelInfo; that shall be
+ * used to track relations to be modified at inheritance_planner().
+ */
+ ListCell *lc1, *lc2;
+
+ foreach (lc1, root->append_rel_list)
+ {
+ AppendRelInfo *apinfo = lfirst(lc1);
+
+ if (apinfo->parent_relid != rtindex)
+ continue;
+
+ if (apply_row_security_relation(root, vartrans, cmd,
+ apinfo->child_relid))
+ {
+ /*
+ * Save the rtindex of actual relation to be modified,
+ * if parent relation is result relation of this query.
+ */
+ if (parse->resultRelation == rtindex)
+ apinfo->child_result = apinfo->child_relid;
+
+ apinfo->child_relid = vartrans[apinfo->child_relid];
+ /* Adjust varno to reference pseudo columns */
+ foreach (lc2, apinfo->translated_vars)
+ {
+ Var *var = lfirst(lc2);
+
+ if (var)
+ var->varno = apinfo->child_relid;
+ }
+ result = true;
+ }
+ }
+ }
+ return result;
+}
+
+/*
+ * apply_row_security_recursive
+ *
+ * It walks on the given join-tree to replace relations with row-level
+ * security policy by a simple sub-query.
+ */
+static bool
+apply_row_security_recursive(PlannerInfo *root, Index *vartrans, Node *jtnode)
+{
+ bool result = false;
+
+ if (jtnode == NULL)
+ return false;
+ if (IsA(jtnode, RangeTblRef))
+ {
+ Index rtindex = ((RangeTblRef *) jtnode)->rtindex;
+ Query *parse = root->parse;
+ RangeTblEntry *rte = rt_fetch(rtindex, parse->rtable);
+ CmdType cmd;
+
+ /* Only relation can have row-security policy */
+ if (rte->rtekind != RTE_RELATION)
+ return false;
+
+ /*
+ * Prevents infinite recursion. Please note that rtindex == 1
+ * of the row-security subquery is a relation being already
+ * processed on the upper level.
+ */
+ if (parse->querySource == QSRC_ROW_SECURITY && rtindex == 1)
+ return false;
+
+ /* Is it a result relation of UPDATE or DELETE command? */
+ if (parse->resultRelation == rtindex)
+ cmd = parse->commandType;
+ else
+ cmd = CMD_SELECT;
+
+ /* Try to apply row-security policy, if configured */
+ result = apply_row_security_relation(root, vartrans, cmd, rtindex);
+ }
+ else if (IsA(jtnode, FromExpr))
+ {
+ FromExpr *f = (FromExpr *) jtnode;
+ ListCell *l;
+
+ foreach (l, f->fromlist)
+ {
+ if (apply_row_security_recursive(root, vartrans, lfirst(l)))
+ result = true;
+ }
+ }
+ else if (IsA(jtnode, JoinExpr))
+ {
+ JoinExpr *j = (JoinExpr *) jtnode;
+
+ if (apply_row_security_recursive(root, vartrans, j->larg))
+ result = true;
+ if (apply_row_security_recursive(root, vartrans, j->rarg))
+ result = true;
+ }
+ else
+ elog(ERROR, "unexpected node type: %d", (int) nodeTag(jtnode));
+
+ return result;
+}
+
+/*
+ * apply_row_security_policy
+ *
+ * Entrypoint to apply configured row-security policy of the relation.
+ *
+ * In case when the supplied query references relations with row-security
+ * policy, its RangeTblEntry shall be replaced by a row-security subquery
+ * that has simple scan on the referenced table with policy qualifiers.
+ * Of course, security-barrier shall be set on the subquery to prevent
+ * unexpected push-down of functions without leakproof flag.
+ *
+ * For example, when table t1 has a security policy "(x % 2 = 0)", the
+ * following query:
+ * SELECT * FROM t1 WHERE f_leak(y)
+ * performs as if
+ * SELECT * FROM (
+ * SELECT x, y FROM t1 WHERE (x % 2 = 0)
+ * ) AS t1 WHERE f_leak(y)
+ * would be given. Because the sub-query has security barrier flag,
+ * configured security policy qualifier is always executed prior to
+ * user given functions.
+ */
+void
+apply_row_security_policy(PlannerInfo *root)
+{
+ Query *parse = root->parse;
+ Oid curr_userid;
+ int curr_seccxt;
+ Index *vartrans;
+
+ /*
+ * Mode checks. In case when SECURITY_ROW_LEVEL_DISABLED is set,
+ * no row-level security policy should be applied regardless
+ * whether it is built-in or extension.
+ */
+ GetUserIdAndSecContext(&curr_userid, &curr_seccxt);
+ if (curr_seccxt & SECURITY_ROW_LEVEL_DISABLED)
+ return;
+
+ vartrans = palloc0(sizeof(Index) * (list_length(parse->rtable) + 1));
+ if (apply_row_security_recursive(root, vartrans, (Node *)parse->jointree))
+ {
+ PlannerGlobal *glob = root->glob;
+ PlanInvalItem *pi_item;
+ fixup_varnode_context context;
+
+ /*
+ * Constructed Plan with row-level security policy depends on
+ * properties of current user (database superuser can bypass
+ * configured row-security policy!), thus, it has to be
+ * invalidated when its assumption was changed.
+ */
+ if (!OidIsValid(glob->planUserId))
+ {
+ /* Plan invalidation on session user-id */
+ glob->planUserId = GetUserId();
+
+ /* Plan invalidation on catalog updates of pg_authid */
+ pi_item = makeNode(PlanInvalItem);
+ pi_item->cacheId = AUTHOID;
+ pi_item->hashValue =
+ GetSysCacheHashValue1(AUTHOID,
+ ObjectIdGetDatum(glob->planUserId));
+ glob->invalItems = lappend(glob->invalItems, pi_item);
+ }
+ else
+ Assert(glob->planUserId == GetUserId());
+
+ /*
+ * Var-nodes that referenced RangeTblEntry to be replaced by
+ * row-security sub-query have to be adjusted for appropriate
+ * reference to the underlying pseudo column of the relation.
+ */
+ context.root = root;
+ context.varlevelsup = 0;
+ context.vartrans = vartrans;
+ query_tree_walker(parse,
+ fixup_varnode_walker,
+ (void *) &context,
+ QTW_IGNORE_RETURNING);
+ }
+ pfree(vartrans);
+}
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index 0b13645..2f56211 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -3282,3 +3282,19 @@ QueryRewrite(Query *parsetree)
return results;
}
+
+/*
+ * QueryRewriteExpr
+ *
+ * This routine provides an entry point of query rewriter towards
+ * a certain expression tree with SubLink node; being added after
+ * the top level query rewrite.
+ * It primarily intends to expand views appeared in the qualifiers
+ * appended with row-level security which needs to modify query
+ * tree at head of the planner stage.
+ */
+void
+QueryRewriteExpr(Node *node, List *activeRIRs)
+{
+ fireRIRonSubLink(node, activeRIRs);
+}
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index 1e1e616..68be040 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -3008,6 +3008,7 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
int spi_result;
Oid save_userid;
int save_sec_context;
+ int temp_sec_context;
Datum vals[RI_MAX_NUMKEYS * 2];
char nulls[RI_MAX_NUMKEYS * 2];
@@ -3087,8 +3088,18 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
/* Switch to proper UID to perform check as */
GetUserIdAndSecContext(&save_userid, &save_sec_context);
+
+ /*
+ * Row-level security should be disabled in case when foreign-key
+ * relation is queried to check existence of tuples that references
+ * the primary-key being modified.
+ */
+ temp_sec_context = save_sec_context | SECURITY_LOCAL_USERID_CHANGE;
+ if (source_is_pk)
+ temp_sec_context |= SECURITY_ROW_LEVEL_DISABLED;
+
SetUserIdAndSecContext(RelationGetForm(query_rel)->relowner,
- save_sec_context | SECURITY_LOCAL_USERID_CHANGE);
+ temp_sec_context);
/* Finally we can run the query. */
spi_result = SPI_execute_snapshot(qplan,
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index f133e5f..1c97bd9 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -272,6 +272,7 @@ extern int trace_recovery(int trace_level);
/* flags to be OR'd to form sec_context */
#define SECURITY_LOCAL_USERID_CHANGE 0x0001
#define SECURITY_RESTRICTED_OPERATION 0x0002
+#define SECURITY_ROW_LEVEL_DISABLED 0x0004
extern char *DatabasePath;
diff --git a/src/include/nodes/nodeFuncs.h b/src/include/nodes/nodeFuncs.h
index f9afdb6..9ed3796 100644
--- a/src/include/nodes/nodeFuncs.h
+++ b/src/include/nodes/nodeFuncs.h
@@ -24,6 +24,7 @@
#define QTW_IGNORE_RANGE_TABLE 0x08 /* skip rangetable entirely */
#define QTW_EXAMINE_RTES 0x10 /* examine RTEs */
#define QTW_DONT_COPY_QUERY 0x20 /* do not copy top Query */
+#define QTW_IGNORE_RETURNING 0x40 /* skip returning clause */
extern Oid exprType(const Node *expr);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 3a918bb..c8ce32a 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -113,7 +113,9 @@ typedef struct Query
int resultRelation; /* rtable index of target relation for
* INSERT/UPDATE/DELETE; 0 for SELECT */
-
+ int sourceRelation; /* rtable index of source relation for
+ * UPDATE/DELETE, if not identical with
+ * resultRelation; 0 for elsewhere */
bool hasAggs; /* has aggregates in tlist or havingQual */
bool hasWindowFuncs; /* has window functions in tlist */
bool hasSubLinks; /* has subquery SubLink */
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 74d0e12..252e45f 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -1438,6 +1438,10 @@ typedef struct AppendRelInfo
*/
Index parent_relid; /* RT index of append parent rel */
Index child_relid; /* RT index of append child rel */
+ Index child_result; /* RT index of append child rel's source,
+ * if source of result relation is not
+ * identical. Elsewhere, 0.
+ */
/*
* For an inheritance appendrel, the parent and child are both regular
diff --git a/src/include/optimizer/rowsecurity.h b/src/include/optimizer/rowsecurity.h
new file mode 100644
index 0000000..ff4dd9a
--- /dev/null
+++ b/src/include/optimizer/rowsecurity.h
@@ -0,0 +1,27 @@
+/* -------------------------------------------------------------------------
+ *
+ * rowsecurity.h
+ * prototypes for optimizer/rowsecurity.c
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * -------------------------------------------------------------------------
+ */
+#ifndef ROWSECURITY_H
+#define ROWSECURITY_H
+
+#include "nodes/execnodes.h"
+#include "nodes/parsenodes.h"
+#include "nodes/relation.h"
+#include "utils/rel.h"
+
+typedef List *(*row_security_policy_hook_type)(CmdType cmdtype,
+ Relation relation);
+extern PGDLLIMPORT row_security_policy_hook_type row_security_policy_hook;
+
+extern bool copy_row_security_policy(CopyStmt *stmt,
+ Relation relation, List *attnums);
+extern void apply_row_security_policy(PlannerInfo *root);
+
+#endif /* ROWSECURITY_H */
diff --git a/src/include/rewrite/rewriteHandler.h b/src/include/rewrite/rewriteHandler.h
index e4027bd..7bd334c 100644
--- a/src/include/rewrite/rewriteHandler.h
+++ b/src/include/rewrite/rewriteHandler.h
@@ -18,6 +18,7 @@
#include "nodes/parsenodes.h"
extern List *QueryRewrite(Query *parsetree);
+extern void QueryRewriteExpr(Node *node, List *activeRIRs);
extern void AcquireRewriteLocks(Query *parsetree, bool forUpdatePushedDown);
extern Node *build_column_default(Relation rel, int attrno);
--
1.8.3.1
0008-RLS-Enforce-row-security-on-COPY.patchtext/x-patch; name=0008-RLS-Enforce-row-security-on-COPY.patchDownload
>From a599392b76b77aae5610b5654120f08ff19a6a92 Mon Sep 17 00:00:00 2001
From: Craig Ringer <craig@2ndquadrant.com>
Date: Mon, 20 Jan 2014 13:50:23 +0800
Subject: [PATCH 08/10] RLS: Enforce row-security on COPY
---
src/backend/commands/copy.c | 90 ++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 88 insertions(+), 2 deletions(-)
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index f91f164..8e98fd2 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -24,6 +24,7 @@
#include "access/htup_details.h"
#include "access/sysattr.h"
#include "access/xact.h"
+#include "catalog/heap.h"
#include "catalog/namespace.h"
#include "catalog/pg_type.h"
#include "commands/copy.h"
@@ -34,15 +35,19 @@
#include "libpq/pqformat.h"
#include "mb/pg_wchar.h"
#include "miscadmin.h"
+#include "nodes/makefuncs.h"
#include "optimizer/clauses.h"
#include "optimizer/planner.h"
+#include "optimizer/rowsecurity.h"
#include "parser/parse_relation.h"
+#include "parser/parsetree.h"
#include "rewrite/rewriteHandler.h"
#include "storage/fd.h"
#include "tcop/tcopprot.h"
#include "utils/acl.h"
#include "utils/builtins.h"
#include "utils/lsyscache.h"
+#include "utils/syscache.h"
#include "utils/memutils.h"
#include "utils/portal.h"
#include "utils/rel.h"
@@ -814,6 +819,21 @@ DoCopy(const CopyStmt *stmt, const char *queryString, uint64 *processed)
rel = heap_openrv(stmt->relation,
(is_from ? RowExclusiveLock : AccessShareLock));
+ tupDesc = RelationGetDescr(rel);
+ attnums = CopyGetAttnums(tupDesc, rel, stmt->attlist);
+
+ /*
+ * We have to run regular query, if the target relation has
+ * row-level security policy
+ */
+ if (copy_row_security_policy((CopyStmt *)stmt, rel, attnums))
+ {
+ heap_close(rel, NoLock); /* close with keeping lock */
+ relid = InvalidOid;
+ rel = NULL;
+ }
+ else
+ {
relid = RelationGetRelid(rel);
rte = makeNode(RangeTblEntry);
@@ -822,8 +842,6 @@ DoCopy(const CopyStmt *stmt, const char *queryString, uint64 *processed)
rte->relkind = rel->rd_rel->relkind;
rte->requiredPerms = required_access;
- tupDesc = RelationGetDescr(rel);
- attnums = CopyGetAttnums(tupDesc, rel, stmt->attlist);
foreach(cur, attnums)
{
int attno = lfirst_int(cur) -
@@ -835,6 +853,7 @@ DoCopy(const CopyStmt *stmt, const char *queryString, uint64 *processed)
rte->selectedCols = bms_add_member(rte->selectedCols, attno);
}
ExecCheckRTPerms(list_make1(rte), true);
+ }
}
else
{
@@ -1193,6 +1212,53 @@ ProcessCopyOptions(CopyState cstate,
}
/*
+ * Adjust Query tree constructed with row-level security feature.
+ * If WITH OIDS option was supplied, it adds Var node to reference
+ * object-id system column.
+ */
+static void
+fixup_oid_of_rls_query(Query *query)
+{
+ RangeTblEntry *subrte;
+ TargetEntry *subtle;
+ Var *subvar;
+ ListCell *cell;
+ Form_pg_attribute attform
+ = SystemAttributeDefinition(ObjectIdAttributeNumber, true);
+
+ subrte = rt_fetch((Index) 1, query->rtable);
+ Assert(subrte->rtekind == RTE_RELATION);
+
+ if (!SearchSysCacheExists2(ATTNUM,
+ ObjectIdGetDatum(subrte->relid),
+ Int16GetDatum(attform->attnum)))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_COLUMN),
+ errmsg("table \"%s\" does not have OIDs",
+ get_rel_name(subrte->relid))));
+
+ subvar = makeVar((Index) 1,
+ attform->attnum,
+ attform->atttypid,
+ attform->atttypmod,
+ attform->attcollation,
+ 0);
+ subtle = makeTargetEntry((Expr *) subvar,
+ 0,
+ pstrdup(NameStr(attform->attname)),
+ false);
+
+ query->targetList = list_concat(list_make1(subtle),
+ query->targetList);
+ /* adjust resno of TargetEntry */
+ foreach (cell, query->targetList)
+ {
+ subtle = lfirst(cell);
+ subtle->resno++;
+ }
+}
+
+/*
* Common setup routines used by BeginCopyFrom and BeginCopyTo.
*
* Iff <binary>, unload or reload in the binary format, as opposed to the
@@ -1264,6 +1330,25 @@ BeginCopy(bool is_from,
Assert(!is_from);
cstate->rel = NULL;
+ /*
+ * In case when regular COPY TO was replaced because of row-level
+ * security, "raw_query" node have already analyzed / rewritten
+ * query tree.
+ */
+ if (IsA(raw_query, Query))
+ {
+ query = (Query *) raw_query;
+
+ Assert(query->querySource == QSRC_ROW_SECURITY);
+ if (cstate->oids)
+ {
+ fixup_oid_of_rls_query(query);
+ cstate->oids = false;
+ }
+ attnamelist = NIL;
+ }
+ else
+ {
/* Don't allow COPY w/ OIDs from a select */
if (cstate->oids)
ereport(ERROR,
@@ -1288,6 +1373,7 @@ BeginCopy(bool is_from,
elog(ERROR, "unexpected rewrite result");
query = (Query *) linitial(rewritten);
+ }
/* The grammar allows SELECT INTO, but we don't support that */
if (query->utilityStmt != NULL &&
--
1.8.3.1
0009-RLS-Regression-tests.patchtext/x-patch; name=0009-RLS-Regression-tests.patchDownload
>From 3afd241dd3a81b64c42a86fbc8a87f997e080397 Mon Sep 17 00:00:00 2001
From: Craig Ringer <craig@2ndquadrant.com>
Date: Mon, 20 Jan 2014 12:58:29 +0800
Subject: [PATCH 09/10] RLS: Regression tests
---
src/test/regress/expected/rowsecurity.out | 952 ++++++++++++++++++++++++++++++
src/test/regress/parallel_schedule | 2 +-
src/test/regress/serial_schedule | 1 +
src/test/regress/sql/rowsecurity.sql | 298 ++++++++++
4 files changed, 1252 insertions(+), 1 deletion(-)
create mode 100644 src/test/regress/expected/rowsecurity.out
create mode 100644 src/test/regress/sql/rowsecurity.sql
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
new file mode 100644
index 0000000..44b2888
--- /dev/null
+++ b/src/test/regress/expected/rowsecurity.out
@@ -0,0 +1,952 @@
+--
+-- Test of Row-level security feature
+--
+-- Clean up in case a prior regression run failed
+-- Suppress NOTICE messages when users/groups don't exist
+SET client_min_messages TO 'warning';
+DROP USER IF EXISTS rls_regress_user0;
+DROP USER IF EXISTS rls_regress_user1;
+DROP USER IF EXISTS rls_regress_user2;
+DROP SCHEMA IF EXISTS rls_regress_schema CASCADE;
+RESET client_min_messages;
+-- initial setup
+CREATE USER rls_regress_user0;
+CREATE USER rls_regress_user1;
+CREATE USER rls_regress_user2;
+CREATE SCHEMA rls_regress_schema;
+GRANT ALL ON SCHEMA rls_regress_schema TO public;
+SET search_path = rls_regress_schema;
+-- setup of malicious function
+CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool
+ COST 0.0000001 LANGUAGE plpgsql
+ AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END';
+GRANT EXECUTE ON FUNCTION f_leak(text) TO public;
+-- BASIC Row-Level Security Scenario
+SET SESSION AUTHORIZATION rls_regress_user0;
+CREATE TABLE uaccount (
+ pguser name primary key,
+ seclv int
+);
+INSERT INTO uaccount VALUES
+ ('rls_regress_user0', 99),
+ ('rls_regress_user1', 1),
+ ('rls_regress_user2', 2),
+ ('rls_regress_user3', 3);
+GRANT SELECT ON uaccount TO public;
+CREATE TABLE category (
+ cid int primary key,
+ cname text
+);
+GRANT ALL ON category TO public;
+INSERT INTO category VALUES
+ (11, 'novel'),
+ (22, 'science fiction'),
+ (33, 'technology'),
+ (44, 'manga');
+CREATE TABLE document (
+ did int primary key,
+ cid int references category(cid),
+ dlevel int not null,
+ dauthor name,
+ dtitle text
+);
+GRANT ALL ON document TO public;
+INSERT INTO document VALUES
+ ( 1, 11, 1, 'rls_regress_user1', 'my first novel'),
+ ( 2, 11, 2, 'rls_regress_user1', 'my second novel'),
+ ( 3, 22, 2, 'rls_regress_user1', 'my science fiction'),
+ ( 4, 44, 1, 'rls_regress_user1', 'my first manga'),
+ ( 5, 44, 2, 'rls_regress_user1', 'my second manga'),
+ ( 6, 22, 1, 'rls_regress_user2', 'great science fiction'),
+ ( 7, 33, 2, 'rls_regress_user2', 'great technology book'),
+ ( 8, 44, 1, 'rls_regress_user2', 'great manga');
+-- user's security level must higher than or equal to document's one
+ALTER TABLE document SET ROW SECURITY FOR ALL
+ TO (dlevel <= (SELECT seclv FROM uaccount WHERE pguser = current_user));
+-- viewpoint from rls_regress_user1
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE: f_leak => my first novel
+NOTICE: f_leak => my first manga
+NOTICE: f_leak => great science fiction
+NOTICE: f_leak => great manga
+ did | cid | dlevel | dauthor | dtitle
+-----+-----+--------+-------------------+-----------------------
+ 1 | 11 | 1 | rls_regress_user1 | my first novel
+ 4 | 44 | 1 | rls_regress_user1 | my first manga
+ 6 | 22 | 1 | rls_regress_user2 | great science fiction
+ 8 | 44 | 1 | rls_regress_user2 | great manga
+(4 rows)
+
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+NOTICE: f_leak => my first novel
+NOTICE: f_leak => my first manga
+NOTICE: f_leak => great science fiction
+NOTICE: f_leak => great manga
+ cid | did | dlevel | dauthor | dtitle | cname
+-----+-----+--------+-------------------+-----------------------+-----------------
+ 11 | 1 | 1 | rls_regress_user1 | my first novel | novel
+ 22 | 6 | 1 | rls_regress_user2 | great science fiction | science fiction
+ 44 | 8 | 1 | rls_regress_user2 | great manga | manga
+ 44 | 4 | 1 | rls_regress_user1 | my first manga | manga
+(4 rows)
+
+-- viewpoint from rls_regress_user2
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE: f_leak => my first novel
+NOTICE: f_leak => my second novel
+NOTICE: f_leak => my science fiction
+NOTICE: f_leak => my first manga
+NOTICE: f_leak => my second manga
+NOTICE: f_leak => great science fiction
+NOTICE: f_leak => great technology book
+NOTICE: f_leak => great manga
+ did | cid | dlevel | dauthor | dtitle
+-----+-----+--------+-------------------+-----------------------
+ 1 | 11 | 1 | rls_regress_user1 | my first novel
+ 2 | 11 | 2 | rls_regress_user1 | my second novel
+ 3 | 22 | 2 | rls_regress_user1 | my science fiction
+ 4 | 44 | 1 | rls_regress_user1 | my first manga
+ 5 | 44 | 2 | rls_regress_user1 | my second manga
+ 6 | 22 | 1 | rls_regress_user2 | great science fiction
+ 7 | 33 | 2 | rls_regress_user2 | great technology book
+ 8 | 44 | 1 | rls_regress_user2 | great manga
+(8 rows)
+
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+NOTICE: f_leak => my first novel
+NOTICE: f_leak => my second novel
+NOTICE: f_leak => my science fiction
+NOTICE: f_leak => my first manga
+NOTICE: f_leak => my second manga
+NOTICE: f_leak => great science fiction
+NOTICE: f_leak => great technology book
+NOTICE: f_leak => great manga
+ cid | did | dlevel | dauthor | dtitle | cname
+-----+-----+--------+-------------------+-----------------------+-----------------
+ 11 | 2 | 2 | rls_regress_user1 | my second novel | novel
+ 11 | 1 | 1 | rls_regress_user1 | my first novel | novel
+ 22 | 6 | 1 | rls_regress_user2 | great science fiction | science fiction
+ 22 | 3 | 2 | rls_regress_user1 | my science fiction | science fiction
+ 33 | 7 | 2 | rls_regress_user2 | great technology book | technology
+ 44 | 8 | 1 | rls_regress_user2 | great manga | manga
+ 44 | 5 | 2 | rls_regress_user1 | my second manga | manga
+ 44 | 4 | 1 | rls_regress_user1 | my first manga | manga
+(8 rows)
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+ QUERY PLAN
+----------------------------------------------------------
+ Subquery Scan on document
+ Filter: f_leak(document.dtitle)
+ -> Seq Scan on document document_1
+ Filter: (dlevel <= $0)
+ InitPlan 1 (returns $0)
+ -> Index Scan using uaccount_pkey on uaccount
+ Index Cond: (pguser = "current_user"())
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+ QUERY PLAN
+----------------------------------------------------------------------
+ Hash Join
+ Hash Cond: (category.cid = document.cid)
+ -> Seq Scan on category
+ -> Hash
+ -> Subquery Scan on document
+ Filter: f_leak(document.dtitle)
+ -> Seq Scan on document document_1
+ Filter: (dlevel <= $0)
+ InitPlan 1 (returns $0)
+ -> Index Scan using uaccount_pkey on uaccount
+ Index Cond: (pguser = "current_user"())
+(11 rows)
+
+-- only owner can change row-level security
+ALTER TABLE document SET ROW SECURITY FOR ALL TO (true); -- fail
+ERROR: must be owner of relation document
+ALTER TABLE document RESET ROW SECURITY FOR ALL; -- fail
+ERROR: must be owner of relation document
+SET SESSION AUTHORIZATION rls_regress_user0;
+ALTER TABLE document SET ROW SECURITY FOR ALL TO (dauthor = current_user);
+-- viewpoint from rls_regress_user1 again
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE: f_leak => my first novel
+NOTICE: f_leak => my second novel
+NOTICE: f_leak => my science fiction
+NOTICE: f_leak => my first manga
+NOTICE: f_leak => my second manga
+ did | cid | dlevel | dauthor | dtitle
+-----+-----+--------+-------------------+--------------------
+ 1 | 11 | 1 | rls_regress_user1 | my first novel
+ 2 | 11 | 2 | rls_regress_user1 | my second novel
+ 3 | 22 | 2 | rls_regress_user1 | my science fiction
+ 4 | 44 | 1 | rls_regress_user1 | my first manga
+ 5 | 44 | 2 | rls_regress_user1 | my second manga
+(5 rows)
+
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+NOTICE: f_leak => my first novel
+NOTICE: f_leak => my second novel
+NOTICE: f_leak => my science fiction
+NOTICE: f_leak => my first manga
+NOTICE: f_leak => my second manga
+ cid | did | dlevel | dauthor | dtitle | cname
+-----+-----+--------+-------------------+--------------------+-----------------
+ 11 | 1 | 1 | rls_regress_user1 | my first novel | novel
+ 11 | 2 | 2 | rls_regress_user1 | my second novel | novel
+ 22 | 3 | 2 | rls_regress_user1 | my science fiction | science fiction
+ 44 | 4 | 1 | rls_regress_user1 | my first manga | manga
+ 44 | 5 | 2 | rls_regress_user1 | my second manga | manga
+(5 rows)
+
+-- viewpoint from rls_regress_user2 again
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE: f_leak => great science fiction
+NOTICE: f_leak => great technology book
+NOTICE: f_leak => great manga
+ did | cid | dlevel | dauthor | dtitle
+-----+-----+--------+-------------------+-----------------------
+ 6 | 22 | 1 | rls_regress_user2 | great science fiction
+ 7 | 33 | 2 | rls_regress_user2 | great technology book
+ 8 | 44 | 1 | rls_regress_user2 | great manga
+(3 rows)
+
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+NOTICE: f_leak => great science fiction
+NOTICE: f_leak => great technology book
+NOTICE: f_leak => great manga
+ cid | did | dlevel | dauthor | dtitle | cname
+-----+-----+--------+-------------------+-----------------------+-----------------
+ 22 | 6 | 1 | rls_regress_user2 | great science fiction | science fiction
+ 33 | 7 | 2 | rls_regress_user2 | great technology book | technology
+ 44 | 8 | 1 | rls_regress_user2 | great manga | manga
+(3 rows)
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+ QUERY PLAN
+----------------------------------------------
+ Subquery Scan on document
+ Filter: f_leak(document.dtitle)
+ -> Seq Scan on document document_1
+ Filter: (dauthor = "current_user"())
+(4 rows)
+
+EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+ QUERY PLAN
+----------------------------------------------------
+ Nested Loop
+ -> Subquery Scan on document
+ Filter: f_leak(document.dtitle)
+ -> Seq Scan on document document_1
+ Filter: (dauthor = "current_user"())
+ -> Index Scan using category_pkey on category
+ Index Cond: (cid = document.cid)
+(7 rows)
+
+-- interaction of FK/PK constraints
+SET SESSION AUTHORIZATION rls_regress_user0;
+ALTER TABLE category SET ROW SECURITY FOR ALL
+ TO (CASE WHEN current_user = 'rls_regress_user1' THEN cid IN (11, 33)
+ WHEN current_user = 'rls_regress_user2' THEN cid IN (22, 44)
+ ELSE false END);
+-- cannot delete PK referenced by invisible FK
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document d full outer join category c on d.cid = c.cid;
+ did | cid | dlevel | dauthor | dtitle | cid | cname
+-----+-----+--------+-------------------+--------------------+-----+------------
+ 2 | 11 | 2 | rls_regress_user1 | my second novel | 11 | novel
+ 1 | 11 | 1 | rls_regress_user1 | my first novel | 11 | novel
+ | | | | | 33 | technology
+ 5 | 44 | 2 | rls_regress_user1 | my second manga | |
+ 4 | 44 | 1 | rls_regress_user1 | my first manga | |
+ 3 | 22 | 2 | rls_regress_user1 | my science fiction | |
+(6 rows)
+
+DELETE FROM category WHERE cid = 33; -- failed
+ERROR: update or delete on table "category" violates foreign key constraint "document_cid_fkey" on table "document"
+DETAIL: Key (cid)=(33) is still referenced from table "document".
+-- cannot insert FK referencing invisible PK
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document d full outer join category c on d.cid = c.cid;
+ did | cid | dlevel | dauthor | dtitle | cid | cname
+-----+-----+--------+-------------------+-----------------------+-----+-----------------
+ 6 | 22 | 1 | rls_regress_user2 | great science fiction | 22 | science fiction
+ 8 | 44 | 1 | rls_regress_user2 | great manga | 44 | manga
+ 7 | 33 | 2 | rls_regress_user2 | great technology book | |
+(3 rows)
+
+INSERT INTO document VALUES (10, 33, 1, current_user, 'hoge'); -- failed
+ERROR: insert or update on table "document" violates foreign key constraint "document_cid_fkey"
+DETAIL: Key (cid)=(33) is not present in table "category".
+-- database superuser can bypass RLS policy
+RESET SESSION AUTHORIZATION;
+SELECT * FROM document;
+ did | cid | dlevel | dauthor | dtitle
+-----+-----+--------+-------------------+-----------------------
+ 1 | 11 | 1 | rls_regress_user1 | my first novel
+ 2 | 11 | 2 | rls_regress_user1 | my second novel
+ 3 | 22 | 2 | rls_regress_user1 | my science fiction
+ 4 | 44 | 1 | rls_regress_user1 | my first manga
+ 5 | 44 | 2 | rls_regress_user1 | my second manga
+ 6 | 22 | 1 | rls_regress_user2 | great science fiction
+ 7 | 33 | 2 | rls_regress_user2 | great technology book
+ 8 | 44 | 1 | rls_regress_user2 | great manga
+(8 rows)
+
+SELECT * FROM category;
+ cid | cname
+-----+-----------------
+ 11 | novel
+ 22 | science fiction
+ 33 | technology
+ 44 | manga
+(4 rows)
+
+--
+-- Table inheritance and RLS policy
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+CREATE TABLE t1 (a int, junk1 text, b text) WITH OIDS;
+ALTER TABLE t1 DROP COLUMN junk1; -- just a disturbing factor
+GRANT ALL ON t1 TO public;
+COPY t1 FROM stdin WITH (oids);
+CREATE TABLE t2 (c float) INHERITS (t1);
+COPY t2 FROM stdin WITH (oids);
+CREATE TABLE t3 (c text, b text, a int) WITH OIDS;
+ALTER TABLE t3 INHERIT t1;
+COPY t3(a,b,c) FROM stdin WITH (oids);
+ALTER TABLE t1 SET ROW SECURITY FOR ALL TO (a % 2 = 0); -- be even number
+ALTER TABLE t2 SET ROW SECURITY FOR ALL TO (a % 2 = 1); -- be odd number
+SELECT * FROM t1;
+ a | b
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1;
+ QUERY PLAN
+-------------------------------------
+ Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+(8 rows)
+
+SELECT * FROM t1 WHERE f_leak(b);
+NOTICE: f_leak => bbb
+NOTICE: f_leak => ddd
+NOTICE: f_leak => abc
+NOTICE: f_leak => cde
+NOTICE: f_leak => xxx
+NOTICE: f_leak => yyy
+NOTICE: f_leak => zzz
+ a | b
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+ QUERY PLAN
+-------------------------------------
+ Append
+ -> Subquery Scan on t1
+ Filter: f_leak(t1.b)
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ Filter: f_leak(t2.b)
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+ Filter: f_leak(b)
+(11 rows)
+
+-- reference to system column
+SELECT oid, * FROM t1;
+ oid | a | b
+-----+---+-----
+ 102 | 2 | bbb
+ 104 | 4 | ddd
+ 201 | 1 | abc
+ 203 | 3 | cde
+ 301 | 1 | xxx
+ 302 | 2 | yyy
+ 303 | 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1;
+ QUERY PLAN
+-------------------------------------
+ Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+(8 rows)
+
+-- reference to whole-row reference
+SELECT *,t1 FROM t1;
+ a | b | t1
+---+-----+---------
+ 2 | bbb | (2,bbb)
+ 4 | ddd | (4,ddd)
+ 1 | abc | (1,abc)
+ 3 | cde | (3,cde)
+ 1 | xxx | (1,xxx)
+ 2 | yyy | (2,yyy)
+ 3 | zzz | (3,zzz)
+(7 rows)
+
+EXPLAIN (costs off) SELECT *,t1 FROM t1;
+ QUERY PLAN
+-------------------------------------
+ Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+(8 rows)
+
+-- for share/update lock
+SELECT * FROM t1 FOR SHARE;
+ a | b
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 FOR SHARE;
+ QUERY PLAN
+-------------------------------------------
+ LockRows
+ -> Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+(9 rows)
+
+SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+NOTICE: f_leak => bbb
+NOTICE: f_leak => ddd
+NOTICE: f_leak => abc
+NOTICE: f_leak => cde
+NOTICE: f_leak => xxx
+NOTICE: f_leak => yyy
+NOTICE: f_leak => zzz
+ a | b
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+ QUERY PLAN
+-------------------------------------------
+ LockRows
+ -> Append
+ -> Subquery Scan on t1
+ Filter: f_leak(t1.b)
+ -> Seq Scan on t1 t1_1
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ Filter: f_leak(t2.b)
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+ Filter: f_leak(b)
+(12 rows)
+
+--
+-- COPY TO statement
+--
+COPY t1 TO stdout;
+2 bbb
+4 ddd
+COPY t1 TO stdout WITH OIDS;
+102 2 bbb
+104 4 ddd
+COPY t2(c,b) TO stdout WITH OIDS;
+201 1.1 abc
+203 3.3 cde
+COPY (SELECT * FROM t1) TO stdout;
+2 bbb
+4 ddd
+1 abc
+3 cde
+1 xxx
+2 yyy
+3 zzz
+COPY document TO stdout WITH OIDS; -- failed (no oid column)
+ERROR: table "document" does not have OIDs
+--
+-- recursive RLS and VIEWs in policy
+--
+CREATE TABLE s1 (a int, b text);
+INSERT INTO s1 (SELECT x, md5(x::text) FROM generate_series(-10,10) x);
+CREATE TABLE s2 (x int, y text);
+INSERT INTO s2 (SELECT x, md5(x::text) FROM generate_series(-6,6) x);
+CREATE VIEW v2 AS SELECT * FROM s2 WHERE y like '%af%';
+ALTER TABLE s1 SET ROW SECURITY FOR ALL
+ TO (a in (select x from s2 where y like '%2f%'));
+ALTER TABLE s2 SET ROW SECURITY FOR ALL
+ TO (x in (select a from s1 where b like '%22%'));
+SELECT * FROM s1 WHERE f_leak(b); -- fail (infinite recursion)
+ERROR: infinite recursion detected for relation "s1"
+ALTER TABLE s2 SET ROW SECURITY FOR ALL TO (x % 2 = 0);
+SELECT * FROM s1 WHERE f_leak(b); -- OK
+NOTICE: f_leak => c81e728d9d4c2f636f067f89cc14862c
+NOTICE: f_leak => a87ff679a2f3e71d9181a67b7542122c
+ a | b
+---+----------------------------------
+ 2 | c81e728d9d4c2f636f067f89cc14862c
+ 4 | a87ff679a2f3e71d9181a67b7542122c
+(2 rows)
+
+EXPLAIN SELECT * FROM only s1 WHERE f_leak(b);
+ QUERY PLAN
+---------------------------------------------------------------------------------------
+ Subquery Scan on s1 (cost=28.55..61.67 rows=205 width=36)
+ Filter: f_leak(s1.b)
+ -> Hash Join (cost=28.55..55.52 rows=615 width=36)
+ Hash Cond: (s1_1.a = s2.x)
+ -> Seq Scan on s1 s1_1 (cost=0.00..22.30 rows=1230 width=36)
+ -> Hash (cost=28.54..28.54 rows=1 width=4)
+ -> HashAggregate (cost=28.53..28.54 rows=1 width=4)
+ Group Key: s2.x
+ -> Subquery Scan on s2 (cost=0.00..28.52 rows=1 width=4)
+ Filter: (s2.y ~~ '%2f%'::text)
+ -> Seq Scan on s2 s2_1 (cost=0.00..28.45 rows=6 width=36)
+ Filter: ((x % 2) = 0)
+(12 rows)
+
+ALTER TABLE s1 SET ROW SECURITY FOR ALL
+ TO (a in (select x from v2)); -- using VIEW in RLS policy
+SELECT * FROM s1 WHERE f_leak(b); -- OK
+NOTICE: f_leak => 0267aaf632e87a63288a08331f22c7c3
+NOTICE: f_leak => 1679091c5a880faf6fb5e6087eb1b2dc
+ a | b
+----+----------------------------------
+ -4 | 0267aaf632e87a63288a08331f22c7c3
+ 6 | 1679091c5a880faf6fb5e6087eb1b2dc
+(2 rows)
+
+EXPLAIN (COSTS OFF) SELECT * FROM s1 WHERE f_leak(b);
+ QUERY PLAN
+----------------------------------------------------------
+ Subquery Scan on s1
+ Filter: f_leak(s1.b)
+ -> Hash Join
+ Hash Cond: (s1_1.a = s2.x)
+ -> Seq Scan on s1 s1_1
+ -> Hash
+ -> HashAggregate
+ Group Key: s2.x
+ -> Subquery Scan on s2
+ Filter: (s2.y ~~ '%af%'::text)
+ -> Seq Scan on s2 s2_1
+ Filter: ((x % 2) = 0)
+(12 rows)
+
+SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+ xx | x | y
+----+----+----------------------------------
+ -6 | -6 | 596a3d04481816330f07e4f97510c28f
+ -4 | -4 | 0267aaf632e87a63288a08331f22c7c3
+ 2 | 2 | c81e728d9d4c2f636f067f89cc14862c
+(3 rows)
+
+EXPLAIN (COSTS OFF) SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+ QUERY PLAN
+--------------------------------------------------------------------
+ Subquery Scan on s2
+ Filter: (s2.y ~~ '%28%'::text)
+ -> Seq Scan on s2 s2_1
+ Filter: ((x % 2) = 0)
+ SubPlan 1
+ -> Limit
+ -> Subquery Scan on s1
+ -> Nested Loop Semi Join
+ Join Filter: (s1_1.a = s2_2.x)
+ -> Seq Scan on s1 s1_1
+ -> Materialize
+ -> Subquery Scan on s2_2
+ Filter: (s2_2.y ~~ '%af%'::text)
+ -> Seq Scan on s2 s2_3
+ Filter: ((x % 2) = 0)
+(15 rows)
+
+ALTER TABLE s2 SET ROW SECURITY FOR ALL
+ TO (x in (select a from s1 where b like '%d2%'));
+SELECT * FROM s1 WHERE f_leak(b); -- fail (infinite recursion via view)
+ERROR: infinite recursion detected for relation "s1"
+-- prepared statement with rls_regress_user0 privilege
+PREPARE p1(int) AS SELECT * FROM t1 WHERE a <= $1;
+EXECUTE p1(2);
+ a | b
+---+-----
+ 2 | bbb
+ 1 | abc
+ 1 | xxx
+ 2 | yyy
+(4 rows)
+
+EXPLAIN (costs off) EXECUTE p1(2);
+ QUERY PLAN
+----------------------------------------------------
+ Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a <= 2) AND ((a % 2) = 0))
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a <= 2) AND ((a % 2) = 1))
+ -> Seq Scan on t3
+ Filter: (a <= 2)
+(9 rows)
+
+-- superuser is allowed to bypass RLS checks
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1 WHERE f_leak(b);
+NOTICE: f_leak => aaa
+NOTICE: f_leak => bbb
+NOTICE: f_leak => ccc
+NOTICE: f_leak => ddd
+NOTICE: f_leak => abc
+NOTICE: f_leak => bcd
+NOTICE: f_leak => cde
+NOTICE: f_leak => def
+NOTICE: f_leak => xxx
+NOTICE: f_leak => yyy
+NOTICE: f_leak => zzz
+ a | b
+---+-----
+ 1 | aaa
+ 2 | bbb
+ 3 | ccc
+ 4 | ddd
+ 1 | abc
+ 2 | bcd
+ 3 | cde
+ 4 | def
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(11 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+ QUERY PLAN
+---------------------------
+ Append
+ -> Seq Scan on t1
+ Filter: f_leak(b)
+ -> Seq Scan on t2
+ Filter: f_leak(b)
+ -> Seq Scan on t3
+ Filter: f_leak(b)
+(7 rows)
+
+-- plan cache should be invalidated
+EXECUTE p1(2);
+ a | b
+---+-----
+ 1 | aaa
+ 2 | bbb
+ 1 | abc
+ 2 | bcd
+ 1 | xxx
+ 2 | yyy
+(6 rows)
+
+EXPLAIN (costs off) EXECUTE p1(2);
+ QUERY PLAN
+--------------------------
+ Append
+ -> Seq Scan on t1
+ Filter: (a <= 2)
+ -> Seq Scan on t2
+ Filter: (a <= 2)
+ -> Seq Scan on t3
+ Filter: (a <= 2)
+(7 rows)
+
+PREPARE p2(int) AS SELECT * FROM t1 WHERE a = $1;
+EXECUTE p2(2);
+ a | b
+---+-----
+ 2 | bbb
+ 2 | bcd
+ 2 | yyy
+(3 rows)
+
+EXPLAIN (costs off) EXECUTE p2(2);
+ QUERY PLAN
+-------------------------
+ Append
+ -> Seq Scan on t1
+ Filter: (a = 2)
+ -> Seq Scan on t2
+ Filter: (a = 2)
+ -> Seq Scan on t3
+ Filter: (a = 2)
+(7 rows)
+
+-- also, case when privilege switch from superuser
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXECUTE p2(2);
+ a | b
+---+-----
+ 2 | bbb
+ 2 | yyy
+(2 rows)
+
+EXPLAIN (costs off) EXECUTE p2(2);
+ QUERY PLAN
+---------------------------------------------------
+ Append
+ -> Subquery Scan on t1
+ -> Seq Scan on t1 t1_1
+ Filter: ((a = 2) AND ((a % 2) = 0))
+ -> Subquery Scan on t2
+ -> Seq Scan on t2 t2_1
+ Filter: ((a = 2) AND ((a % 2) = 1))
+ -> Seq Scan on t3
+ Filter: (a = 2)
+(9 rows)
+
+--
+-- UPDATE / DELETE and Row-level security
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXPLAIN (costs off) UPDATE t1 SET b = b || b WHERE f_leak(b);
+ QUERY PLAN
+-------------------------------------
+ Update on t1
+ -> Subquery Scan on t1_1
+ Filter: f_leak(t1_1.b)
+ -> Seq Scan on t1 t1_2
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ Filter: f_leak(t2.b)
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+ Filter: f_leak(b)
+(11 rows)
+
+UPDATE t1 SET b = b || b WHERE f_leak(b);
+NOTICE: f_leak => bbb
+NOTICE: f_leak => ddd
+NOTICE: f_leak => abc
+NOTICE: f_leak => cde
+NOTICE: f_leak => xxx
+NOTICE: f_leak => yyy
+NOTICE: f_leak => zzz
+EXPLAIN (costs off) UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b);
+ QUERY PLAN
+-------------------------------------
+ Update on t1
+ -> Subquery Scan on t1_1
+ Filter: f_leak(t1_1.b)
+ -> Seq Scan on t1 t1_2
+ Filter: ((a % 2) = 0)
+(5 rows)
+
+UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b);
+NOTICE: f_leak => bbbbbb
+NOTICE: f_leak => dddddd
+-- returning clause with system column
+UPDATE only t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+NOTICE: f_leak => bbbbbb_updt
+NOTICE: f_leak => dddddd_updt
+ oid | a | b | t1
+-----+---+-------------+-----------------
+ 102 | 2 | bbbbbb_updt | (2,bbbbbb_updt)
+ 104 | 4 | dddddd_updt | (4,dddddd_updt)
+(2 rows)
+
+UPDATE t1 SET b = b WHERE f_leak(b) RETURNING *;
+NOTICE: f_leak => bbbbbb_updt
+NOTICE: f_leak => dddddd_updt
+NOTICE: f_leak => abcabc
+NOTICE: f_leak => cdecde
+NOTICE: f_leak => xxxxxx
+NOTICE: f_leak => yyyyyy
+NOTICE: f_leak => zzzzzz
+ a | b
+---+-------------
+ 2 | bbbbbb_updt
+ 4 | dddddd_updt
+ 1 | abcabc
+ 3 | cdecde
+ 1 | xxxxxx
+ 2 | yyyyyy
+ 3 | zzzzzz
+(7 rows)
+
+UPDATE t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+NOTICE: f_leak => bbbbbb_updt
+NOTICE: f_leak => dddddd_updt
+NOTICE: f_leak => abcabc
+NOTICE: f_leak => cdecde
+NOTICE: f_leak => xxxxxx
+NOTICE: f_leak => yyyyyy
+NOTICE: f_leak => zzzzzz
+ oid | a | b | t1
+-----+---+-------------+-----------------
+ 102 | 2 | bbbbbb_updt | (2,bbbbbb_updt)
+ 104 | 4 | dddddd_updt | (4,dddddd_updt)
+ 201 | 1 | abcabc | (1,abcabc)
+ 203 | 3 | cdecde | (3,cdecde)
+ 301 | 1 | xxxxxx | (1,xxxxxx)
+ 302 | 2 | yyyyyy | (2,yyyyyy)
+ 303 | 3 | zzzzzz | (3,zzzzzz)
+(7 rows)
+
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1;
+ a | b
+---+-------------
+ 1 | aaa
+ 3 | ccc
+ 2 | bbbbbb_updt
+ 4 | dddddd_updt
+ 2 | bcd
+ 4 | def
+ 1 | abcabc
+ 3 | cdecde
+ 1 | xxxxxx
+ 2 | yyyyyy
+ 3 | zzzzzz
+(11 rows)
+
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXPLAIN (costs off) DELETE FROM only t1 WHERE f_leak(b);
+ QUERY PLAN
+-------------------------------------
+ Delete on t1
+ -> Subquery Scan on t1_1
+ Filter: f_leak(t1_1.b)
+ -> Seq Scan on t1 t1_2
+ Filter: ((a % 2) = 0)
+(5 rows)
+
+EXPLAIN (costs off) DELETE FROM t1 WHERE f_leak(b);
+ QUERY PLAN
+-------------------------------------
+ Delete on t1
+ -> Subquery Scan on t1_1
+ Filter: f_leak(t1_1.b)
+ -> Seq Scan on t1 t1_2
+ Filter: ((a % 2) = 0)
+ -> Subquery Scan on t2
+ Filter: f_leak(t2.b)
+ -> Seq Scan on t2 t2_1
+ Filter: ((a % 2) = 1)
+ -> Seq Scan on t3
+ Filter: f_leak(b)
+(11 rows)
+
+DELETE FROM only t1 WHERE f_leak(b) RETURNING oid, *, t1;
+NOTICE: f_leak => bbbbbb_updt
+NOTICE: f_leak => dddddd_updt
+ oid | a | b | t1
+-----+---+-------------+-----------------
+ 102 | 2 | bbbbbb_updt | (2,bbbbbb_updt)
+ 104 | 4 | dddddd_updt | (4,dddddd_updt)
+(2 rows)
+
+DELETE FROM t1 WHERE f_leak(b) RETURNING oid, *, t1;
+NOTICE: f_leak => abcabc
+NOTICE: f_leak => cdecde
+NOTICE: f_leak => xxxxxx
+NOTICE: f_leak => yyyyyy
+NOTICE: f_leak => zzzzzz
+ oid | a | b | t1
+-----+---+--------+------------
+ 201 | 1 | abcabc | (1,abcabc)
+ 203 | 3 | cdecde | (3,cdecde)
+ 301 | 1 | xxxxxx | (1,xxxxxx)
+ 302 | 2 | yyyyyy | (2,yyyyyy)
+ 303 | 3 | zzzzzz | (3,zzzzzz)
+(5 rows)
+
+--
+-- Test psql \dt+ command
+--
+ALTER TABLE category RESET ROW SECURITY FOR ALL; -- too long qual
+\dt+
+ List of relations
+ Schema | Name | Type | Owner | Size | Description | Row-security
+--------------------+----------+-------+-------------------+------------+-------------+----------------------------------
+ rls_regress_schema | category | table | rls_regress_user0 | 16 kB | |
+ rls_regress_schema | document | table | rls_regress_user0 | 16 kB | | (dauthor = "current_user"())
+ rls_regress_schema | s1 | table | rls_regress_user0 | 16 kB | | (a IN ( SELECT v2.x +
+ | | | | | | FROM v2))
+ rls_regress_schema | s2 | table | rls_regress_user0 | 16 kB | | (x IN ( SELECT s1.a +
+ | | | | | | FROM s1 +
+ | | | | | | WHERE (s1.b ~~ '%d2%'::text)))
+ rls_regress_schema | t1 | table | rls_regress_user0 | 16 kB | | ((a % 2) = 0)
+ rls_regress_schema | t2 | table | rls_regress_user0 | 16 kB | | ((a % 2) = 1)
+ rls_regress_schema | t3 | table | rls_regress_user0 | 16 kB | |
+ rls_regress_schema | uaccount | table | rls_regress_user0 | 8192 bytes | |
+(8 rows)
+
+--
+-- Clean up objects
+--
+RESET SESSION AUTHORIZATION;
+DROP SCHEMA rls_regress_schema CASCADE;
+NOTICE: drop cascades to 10 other objects
+DETAIL: drop cascades to function f_leak(text)
+drop cascades to table uaccount
+drop cascades to table category
+drop cascades to table document
+drop cascades to table t1
+drop cascades to table t2
+drop cascades to table t3
+drop cascades to table s1
+drop cascades to table s2
+drop cascades to view v2
+DROP USER rls_regress_user0;
+DROP USER rls_regress_user1;
+DROP USER rls_regress_user2;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 5758b07..f7963fc 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 matview lock replica_identity
+test: privileges rowsecurity security_label collate matview lock replica_identity
# ----------
# Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 78348f5..51c4c12 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -94,6 +94,7 @@ test: delete
test: namespace
test: prepared_xacts
test: privileges
+test: rowsecurity
test: security_label
test: collate
test: matview
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
new file mode 100644
index 0000000..55d4aad
--- /dev/null
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -0,0 +1,298 @@
+--
+-- Test of Row-level security feature
+--
+
+-- Clean up in case a prior regression run failed
+
+-- Suppress NOTICE messages when users/groups don't exist
+SET client_min_messages TO 'warning';
+
+DROP USER IF EXISTS rls_regress_user0;
+DROP USER IF EXISTS rls_regress_user1;
+DROP USER IF EXISTS rls_regress_user2;
+
+DROP SCHEMA IF EXISTS rls_regress_schema CASCADE;
+
+RESET client_min_messages;
+
+-- initial setup
+CREATE USER rls_regress_user0;
+CREATE USER rls_regress_user1;
+CREATE USER rls_regress_user2;
+
+CREATE SCHEMA rls_regress_schema;
+GRANT ALL ON SCHEMA rls_regress_schema TO public;
+SET search_path = rls_regress_schema;
+
+-- setup of malicious function
+CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool
+ COST 0.0000001 LANGUAGE plpgsql
+ AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END';
+GRANT EXECUTE ON FUNCTION f_leak(text) TO public;
+
+-- BASIC Row-Level Security Scenario
+
+SET SESSION AUTHORIZATION rls_regress_user0;
+CREATE TABLE uaccount (
+ pguser name primary key,
+ seclv int
+);
+INSERT INTO uaccount VALUES
+ ('rls_regress_user0', 99),
+ ('rls_regress_user1', 1),
+ ('rls_regress_user2', 2),
+ ('rls_regress_user3', 3);
+GRANT SELECT ON uaccount TO public;
+
+CREATE TABLE category (
+ cid int primary key,
+ cname text
+);
+GRANT ALL ON category TO public;
+INSERT INTO category VALUES
+ (11, 'novel'),
+ (22, 'science fiction'),
+ (33, 'technology'),
+ (44, 'manga');
+
+CREATE TABLE document (
+ did int primary key,
+ cid int references category(cid),
+ dlevel int not null,
+ dauthor name,
+ dtitle text
+);
+GRANT ALL ON document TO public;
+INSERT INTO document VALUES
+ ( 1, 11, 1, 'rls_regress_user1', 'my first novel'),
+ ( 2, 11, 2, 'rls_regress_user1', 'my second novel'),
+ ( 3, 22, 2, 'rls_regress_user1', 'my science fiction'),
+ ( 4, 44, 1, 'rls_regress_user1', 'my first manga'),
+ ( 5, 44, 2, 'rls_regress_user1', 'my second manga'),
+ ( 6, 22, 1, 'rls_regress_user2', 'great science fiction'),
+ ( 7, 33, 2, 'rls_regress_user2', 'great technology book'),
+ ( 8, 44, 1, 'rls_regress_user2', 'great manga');
+
+-- user's security level must higher than or equal to document's one
+ALTER TABLE document SET ROW SECURITY FOR ALL
+ TO (dlevel <= (SELECT seclv FROM uaccount WHERE pguser = current_user));
+
+-- viewpoint from rls_regress_user1
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+-- viewpoint from rls_regress_user2
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+-- only owner can change row-level security
+ALTER TABLE document SET ROW SECURITY FOR ALL TO (true); -- fail
+ALTER TABLE document RESET ROW SECURITY FOR ALL; -- fail
+
+SET SESSION AUTHORIZATION rls_regress_user0;
+ALTER TABLE document SET ROW SECURITY FOR ALL TO (dauthor = current_user);
+
+-- viewpoint from rls_regress_user1 again
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+-- viewpoint from rls_regress_user2 again
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+-- interaction of FK/PK constraints
+SET SESSION AUTHORIZATION rls_regress_user0;
+ALTER TABLE category SET ROW SECURITY FOR ALL
+ TO (CASE WHEN current_user = 'rls_regress_user1' THEN cid IN (11, 33)
+ WHEN current_user = 'rls_regress_user2' THEN cid IN (22, 44)
+ ELSE false END);
+
+-- cannot delete PK referenced by invisible FK
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document d full outer join category c on d.cid = c.cid;
+DELETE FROM category WHERE cid = 33; -- failed
+
+-- cannot insert FK referencing invisible PK
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document d full outer join category c on d.cid = c.cid;
+INSERT INTO document VALUES (10, 33, 1, current_user, 'hoge'); -- failed
+
+-- database superuser can bypass RLS policy
+RESET SESSION AUTHORIZATION;
+SELECT * FROM document;
+SELECT * FROM category;
+
+--
+-- Table inheritance and RLS policy
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+
+CREATE TABLE t1 (a int, junk1 text, b text) WITH OIDS;
+ALTER TABLE t1 DROP COLUMN junk1; -- just a disturbing factor
+GRANT ALL ON t1 TO public;
+
+COPY t1 FROM stdin WITH (oids);
+101 1 aaa
+102 2 bbb
+103 3 ccc
+104 4 ddd
+\.
+
+CREATE TABLE t2 (c float) INHERITS (t1);
+COPY t2 FROM stdin WITH (oids);
+201 1 abc 1.1
+202 2 bcd 2.2
+203 3 cde 3.3
+204 4 def 4.4
+\.
+
+CREATE TABLE t3 (c text, b text, a int) WITH OIDS;
+ALTER TABLE t3 INHERIT t1;
+COPY t3(a,b,c) FROM stdin WITH (oids);
+301 1 xxx X
+302 2 yyy Y
+303 3 zzz Z
+\.
+
+ALTER TABLE t1 SET ROW SECURITY FOR ALL TO (a % 2 = 0); -- be even number
+ALTER TABLE t2 SET ROW SECURITY FOR ALL TO (a % 2 = 1); -- be odd number
+
+SELECT * FROM t1;
+EXPLAIN (costs off) SELECT * FROM t1;
+
+SELECT * FROM t1 WHERE f_leak(b);
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+
+-- reference to system column
+SELECT oid, * FROM t1;
+EXPLAIN (costs off) SELECT * FROM t1;
+
+-- reference to whole-row reference
+SELECT *,t1 FROM t1;
+EXPLAIN (costs off) SELECT *,t1 FROM t1;
+
+-- for share/update lock
+SELECT * FROM t1 FOR SHARE;
+EXPLAIN (costs off) SELECT * FROM t1 FOR SHARE;
+
+SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+
+--
+-- COPY TO statement
+--
+COPY t1 TO stdout;
+COPY t1 TO stdout WITH OIDS;
+COPY t2(c,b) TO stdout WITH OIDS;
+COPY (SELECT * FROM t1) TO stdout;
+COPY document TO stdout WITH OIDS; -- failed (no oid column)
+
+--
+-- recursive RLS and VIEWs in policy
+--
+CREATE TABLE s1 (a int, b text);
+INSERT INTO s1 (SELECT x, md5(x::text) FROM generate_series(-10,10) x);
+
+CREATE TABLE s2 (x int, y text);
+INSERT INTO s2 (SELECT x, md5(x::text) FROM generate_series(-6,6) x);
+CREATE VIEW v2 AS SELECT * FROM s2 WHERE y like '%af%';
+
+ALTER TABLE s1 SET ROW SECURITY FOR ALL
+ TO (a in (select x from s2 where y like '%2f%'));
+
+ALTER TABLE s2 SET ROW SECURITY FOR ALL
+ TO (x in (select a from s1 where b like '%22%'));
+
+SELECT * FROM s1 WHERE f_leak(b); -- fail (infinite recursion)
+
+ALTER TABLE s2 SET ROW SECURITY FOR ALL TO (x % 2 = 0);
+
+SELECT * FROM s1 WHERE f_leak(b); -- OK
+EXPLAIN SELECT * FROM only s1 WHERE f_leak(b);
+
+ALTER TABLE s1 SET ROW SECURITY FOR ALL
+ TO (a in (select x from v2)); -- using VIEW in RLS policy
+SELECT * FROM s1 WHERE f_leak(b); -- OK
+EXPLAIN (COSTS OFF) SELECT * FROM s1 WHERE f_leak(b);
+
+SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+EXPLAIN (COSTS OFF) SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+
+ALTER TABLE s2 SET ROW SECURITY FOR ALL
+ TO (x in (select a from s1 where b like '%d2%'));
+SELECT * FROM s1 WHERE f_leak(b); -- fail (infinite recursion via view)
+
+-- prepared statement with rls_regress_user0 privilege
+PREPARE p1(int) AS SELECT * FROM t1 WHERE a <= $1;
+EXECUTE p1(2);
+EXPLAIN (costs off) EXECUTE p1(2);
+
+-- superuser is allowed to bypass RLS checks
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1 WHERE f_leak(b);
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+
+-- plan cache should be invalidated
+EXECUTE p1(2);
+EXPLAIN (costs off) EXECUTE p1(2);
+
+PREPARE p2(int) AS SELECT * FROM t1 WHERE a = $1;
+EXECUTE p2(2);
+EXPLAIN (costs off) EXECUTE p2(2);
+
+-- also, case when privilege switch from superuser
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXECUTE p2(2);
+EXPLAIN (costs off) EXECUTE p2(2);
+
+--
+-- UPDATE / DELETE and Row-level security
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXPLAIN (costs off) UPDATE t1 SET b = b || b WHERE f_leak(b);
+UPDATE t1 SET b = b || b WHERE f_leak(b);
+
+EXPLAIN (costs off) UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b);
+UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b);
+
+-- returning clause with system column
+UPDATE only t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+UPDATE t1 SET b = b WHERE f_leak(b) RETURNING *;
+UPDATE t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1;
+
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXPLAIN (costs off) DELETE FROM only t1 WHERE f_leak(b);
+EXPLAIN (costs off) DELETE FROM t1 WHERE f_leak(b);
+
+DELETE FROM only t1 WHERE f_leak(b) RETURNING oid, *, t1;
+DELETE FROM t1 WHERE f_leak(b) RETURNING oid, *, t1;
+
+--
+-- Test psql \dt+ command
+--
+ALTER TABLE category RESET ROW SECURITY FOR ALL; -- too long qual
+\dt+
+
+--
+-- Clean up objects
+--
+RESET SESSION AUTHORIZATION;
+
+DROP SCHEMA rls_regress_schema CASCADE;
+
+DROP USER rls_regress_user0;
+DROP USER rls_regress_user1;
+DROP USER rls_regress_user2;
--
1.8.3.1
On 01/24/2014 10:12 AM, Craig Ringer wrote:
(Re-sending; I forgot to cc the list)
On 01/20/2014 02:15 PM, Craig Ringer wrote:
On 01/20/2014 09:58 AM, Craig Ringer wrote:
As it is I'm spending today reworking the RLS patch on top of the new
approach to updatable security barrier views.To get that rolling I've split the RLS patch up into chunks, so we can
argue about the catalogs, ALTER syntax, and the actual row-filtering
implementation separately ;-)It's currently on git@github.com:ringerc/postgres.git in the branch
rls-9.4-split, which is subject to rebasing. I'm still going through it
making sure each chunk at least compiles and preferably passes "make
check".That branch is now pretty stable, and passes checks at every stage up to
the new RLS regression tests. I've pushed a new version to branch
rls-9.4-split. Further updates will rebase this branch.The tag rls-9.4-split-v5 identifies this particular push, and won't get
rebased away.
Pushed a new rebase to the main working branch, merging in the fixes I
made to KaiGai's patch last time.
Tagged rls-9.4-split-v6
I haven't bothered with a patchset for this one, I'll be replacing it
again soon. This is just for anybody following along.
--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hi all
I've hit an interesting wrinkle and am interested in opinions. By
integrating updatable security barrier view support with row-security,
it has become a lot harder to detect and prevent infinite recursion
(including mutual recursion) in row-security policy expansion.
The code is attached, but it's not an independent patch so it's way
easier to grab it from git:
git@github.com:ringerc/postgres.git
branch rls-9.4-upd-sb-views (subject to rebase); or
tag rls-9.4-upd-sb-views-v1
(Only contains half the old row-security patch; I'll rebase the docs,
tests, etc on top of this once it's working properly).
I've integrated the updatable security barrier view support into RLS by
injecting securityQuals in subquery_planner() just before
preprocess_rowmarks . (I'm still thinking about some inheritance related
aspects to that, but that's for later).
The problem is that this causes infinite recursion - the securityQuals
get expanded into a subquery over the original RTE that had the
row-security policy on it. Then subquery_planner is re-entered when
processing the subquery, a row-security qual is found on the target RTE,
and ... *boom*.
Fixing this is not as simple as suppressing expansion of row-security
policy when processing a security barrier subquery created by a
row-security policy, as it is desirable to respect the row-security
policy of *other* tables that get referenced in the expanded
row-security qual.
If we just record the relid of the relation a subquery was expanded from
and avoid expanding that inside the generated subquery we prevent simple
linear recursion, but not mutual recursion between two or more rels with
row-security policy.
KaiGai's row-security patch handles this because it does its own
recursive expansion, so (like the rewriter) it can keep a breadcrumb
trail and detect when it is repeating a path. That's not so practical
when row-security code tags RTEs with policy, then updatable s.b. views
goes and expands them.
So. Options.
1.Come up with a reasonable way to track recursive row-security
expansion, detect infinite recursion, and bail out. Likely to involve
a new global structure with planner/optimizer lifetime that gets
checked and maintained by apply_row_security_rangetbl.
2.Solve the linear recursion case by storing a relid that should not get
expanded for security quals when processing a subquery. Say "don't do
that, expect stack exceeded crashes if you do" for the mutual
recursion case. Seems user hostile, incomplete, and likely to break
people's systems when they really don't expect it.
3.Disregard row-security policy on referenced tables when expanding
row-security qualifiers. There's precendent here in foreign keys,
which ignore row-security policy, but I don't think this is at all
appealing.
4.Magic?
Unless someone has a suggestion for #4, I'll be going with #1. I'd
appreciate tips on doing this in a sane and efficient manner if anyone
has them. I'll be reading over the rewriter's infinite loop protection
and that of KaiGai's RLS patch for ideas in the mean time, and will give
it a good go.
BTW, I feel like we should be letting the rewriter do this job; it's
good at dealing with recursion problems already. That won't work so long
as security barrier qual expansion happens during planning, not rewrite,
though - and we've already explored the fun problems with doing upd.
s.b. qual expansion in rewrite.
--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
Attachments:
0001-RLS-First-attempt-to-integrate-with-updatable-s.b.-v.patchtext/x-patch; name=0001-RLS-First-attempt-to-integrate-with-updatable-s.b.-v.patchDownload
>From 4f8ed71e4cf18db7a4285da9b9c9f8f7f1030e32 Mon Sep 17 00:00:00 2001
From: Craig Ringer <craig@2ndquadrant.com>
Date: Fri, 24 Jan 2014 16:18:40 +0800
Subject: [PATCH] RLS: First attempt to integrate with updatable s.b. views
Enters infinite recursion because the rls-protected table gets expanded
and the new RTE gets checked for row-security quals, gets securityQuals added,
and ... infinite recursion!
---
src/backend/optimizer/plan/planner.c | 7 ++
src/backend/optimizer/prep/prepunion.c | 6 +
src/backend/optimizer/util/Makefile | 3 +-
src/backend/optimizer/util/rowsecurity.c | 185 +++++++++++++++++++++++++++++++
src/include/optimizer/rowsecurity.h | 27 +++++
5 files changed, 227 insertions(+), 1 deletion(-)
create mode 100644 src/backend/optimizer/util/rowsecurity.c
create mode 100644 src/include/optimizer/rowsecurity.h
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 80940ea..8cdd580 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,
}
/*
+ * Check RTEs for row-security policies and set securityQuals on the
+ * RTE if a policy is found. This must happen before inherited table
+ * expansion so that the quals get copied to the child rels.
+ */
+ apply_row_security_policy(root);
+
+ /*
* Preprocess RowMark information. We need to do this after subquery
* pullup (so that all non-inherited RTEs are present) and before
* inheritance expansion (so that the info is available for
diff --git a/src/backend/optimizer/prep/prepunion.c b/src/backend/optimizer/prep/prepunion.c
index c7e0199..20a634d 100644
--- a/src/backend/optimizer/prep/prepunion.c
+++ b/src/backend/optimizer/prep/prepunion.c
@@ -1349,6 +1349,12 @@ expand_inherited_rtentry(PlannerInfo *root, RangeTblEntry *rte, Index rti)
childRTindex = list_length(parse->rtable);
/*
+ * Check for row-security quals on the newly added child
+ * relation and update securityQuals as appropriate.
+ */
+ apply_row_security_rte(root, childRTindex);
+
+ /*
* Build an AppendRelInfo for this parent and child.
*/
appinfo = makeNode(AppendRelInfo);
diff --git a/src/backend/optimizer/util/Makefile b/src/backend/optimizer/util/Makefile
index c54d0a6..d42a285 100644
--- a/src/backend/optimizer/util/Makefile
+++ b/src/backend/optimizer/util/Makefile
@@ -13,6 +13,7 @@ top_builddir = ../../../..
include $(top_builddir)/src/Makefile.global
OBJS = clauses.o joininfo.o orclauses.o pathnode.o placeholder.o \
- plancat.o predtest.o relnode.o restrictinfo.o tlist.o var.o
+ plancat.o predtest.o relnode.o restrictinfo.o tlist.o var.o \
+ rowsecurity.o
include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/optimizer/util/rowsecurity.c b/src/backend/optimizer/util/rowsecurity.c
new file mode 100644
index 0000000..f002891
--- /dev/null
+++ b/src/backend/optimizer/util/rowsecurity.c
@@ -0,0 +1,185 @@
+/*
+ * rewrite/rowsecurity.c
+ * Routines to support row-security feature
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ */
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "access/htup_details.h"
+#include "access/sysattr.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_inherits_fn.h"
+#include "catalog/pg_rowsecurity.h"
+#include "catalog/pg_type.h"
+#include "miscadmin.h"
+#include "nodes/makefuncs.h"
+#include "nodes/nodeFuncs.h"
+#include "nodes/plannodes.h"
+#include "optimizer/clauses.h"
+#include "optimizer/prep.h"
+#include "optimizer/rowsecurity.h"
+#include "parser/parsetree.h"
+#include "rewrite/rewriteHandler.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "tcop/utility.h"
+
+/* hook to allow extensions to apply their own security policy */
+row_security_policy_hook_type row_security_policy_hook = NULL;
+
+/*
+ * pull_row_security_policy
+ *
+ * Fetches the configured row-security policy of both built-in catalogs and any
+ * extensions. If any policy is found a list of qualifier expressions is
+ * returned, where each is treated as a securityQual.
+ */
+static List *
+pull_row_security_policy(CmdType cmd, Relation relation)
+{
+ List *quals = NIL;
+ Expr *qual = NULL;
+
+ /*
+ * Pull the row-security policy configured with built-in features,
+ * if unprivileged users. Please note that superuser can bypass it.
+ */
+ if (relation->rsdesc && !superuser())
+ {
+ RowSecurityDesc *rsdesc = relation->rsdesc;
+ qual = copyObject(rsdesc->rsall.qual);
+ quals = lcons(qual, quals);
+ }
+
+ /*
+ * Also, ask extensions whether they want to apply their own
+ * row-security policy. If both built-in and extension has
+ * their own policy they're applied as nested qualifiers.
+ */
+ if (row_security_policy_hook)
+ {
+ List *temp;
+
+ temp = (*row_security_policy_hook)(cmd, relation);
+ if (temp != NIL)
+ lcons(temp, quals);
+ }
+ return quals;
+}
+
+/*
+ * apply_row_security_rte
+ *
+ * Apply row-security policy to a RangeTblEntry if policy for the
+ * RTE's Relation exists.
+ *
+ * In addition to the initial RTE scan in subquery planner, this also
+ * gets called directly from expand_inherited_rtentry to ensure
+ * that row-security quals applied to children get added.
+ */
+bool
+apply_row_security_rte(PlannerInfo *root, RangeTblEntry* rte)
+{
+ Relation rel;
+ List *rowsecquals;
+ bool result = false;
+
+ Assert(rte->rtekind == RTE_RELATION);
+ rel = heap_open(rte->relid, NoLock);
+ rowsecquals = pull_row_security_policy(root->parse->commandType, rel);
+ if (rowsecquals)
+ {
+ rte->securityQuals = lcons(rowsecquals, rte->securityQuals);
+ result = true;
+ }
+ heap_close(rel, NoLock);
+ return result;
+}
+
+/*
+ * Scan the rangetable of the Query, looking for relations with
+ * row-security policies. Where a policy is found, add its securityQuals
+ * to the RTE then recurse into the quals
+ */
+static bool
+apply_row_security_rangetbl(PlannerInfo *root)
+{
+ ListCell *lc;
+ RangeTblEntry * rte;
+ bool policy_found = false;
+
+ foreach (lc, root->parse->rtable)
+ {
+ rte = (RangeTblEntry*) lfirst(lc);
+ /* Only act on normal tables that are not builtin system catalogs */
+ if (rte->rtekind == RTE_RELATION && rte->relid >= FirstNormalObjectId)
+ policy_found &= apply_row_security_rte(root, rte);
+ }
+ return policy_found;
+}
+
+/*
+ * apply_row_security_policy
+ *
+ * Entrypoint to apply configured row-security policy of the relation.
+ *
+ * In the case when the supplied query references relations with row-security
+ * policy, the relation's RangeTblEntry has the row-security qualifiers
+ * appended to the RTE's securityQual list. This will cause the optimizer to
+ * replace the relation with a security_barrier subquery over the original
+ * relation
+ *
+ * That happens after inheritance expansion has occurred, so any qual on a
+ * parent relation automatically applies to the child as the RTE is copied.
+ * RLS quals are checked for during inheritance expansion and any child
+ * table-specific quals are applied then, producing a second level of
+ * security_barrier subquery.
+ */
+void
+apply_row_security_policy(PlannerInfo *root)
+{
+ Query *parse = root->parse;
+ Oid curr_userid;
+ int curr_seccxt;
+
+ /*
+ * Mode checks. In case when SECURITY_ROW_LEVEL_DISABLED is set,
+ * no row-level security policy should be applied regardless
+ * whether it is built-in or extension.
+ */
+ GetUserIdAndSecContext(&curr_userid, &curr_seccxt);
+ if (curr_seccxt & SECURITY_ROW_LEVEL_DISABLED)
+ return;
+
+ if (apply_row_security_rangetbl(root))
+ {
+ /*
+ * We added at least one securityQual, making the plan
+ * potentially dependent on whether the current user
+ * is row-security exempt.
+ */
+
+ PlannerGlobal *glob = root->glob;
+ PlanInvalItem *pi_item;
+
+ if (!OidIsValid(glob->planUserId))
+ {
+ /* Plan invalidation on session user-id */
+ glob->planUserId = GetUserId();
+
+ /* Plan invalidation on catalog updates of pg_authid */
+ pi_item = makeNode(PlanInvalItem);
+ pi_item->cacheId = AUTHOID;
+ pi_item->hashValue =
+ GetSysCacheHashValue1(AUTHOID,
+ ObjectIdGetDatum(glob->planUserId));
+ glob->invalItems = lappend(glob->invalItems, pi_item);
+ }
+ else
+ Assert(glob->planUserId == GetUserId());
+ }
+}
diff --git a/src/include/optimizer/rowsecurity.h b/src/include/optimizer/rowsecurity.h
new file mode 100644
index 0000000..a996479
--- /dev/null
+++ b/src/include/optimizer/rowsecurity.h
@@ -0,0 +1,27 @@
+/* -------------------------------------------------------------------------
+ *
+ * rowsecurity.h
+ * prototypes for optimizer/rowsecurity.c
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * -------------------------------------------------------------------------
+ */
+#ifndef ROWSECURITY_H
+#define ROWSECURITY_H
+
+#include "nodes/execnodes.h"
+#include "nodes/parsenodes.h"
+#include "nodes/relation.h"
+#include "utils/rel.h"
+
+typedef List *(*row_security_policy_hook_type)(CmdType cmdtype,
+ Relation relation);
+extern PGDLLIMPORT row_security_policy_hook_type row_security_policy_hook;
+
+extern bool apply_row_security_rte(PlannerInfo *root, RangeTblEntry* rte);
+
+extern void apply_row_security_policy(PlannerInfo *root);
+
+#endif /* ROWSECURITY_H */
--
1.8.3.1
On 24 January 2014 09:04, Craig Ringer <craig@2ndquadrant.com> wrote:
Hi all
I've hit an interesting wrinkle and am interested in opinions. By
integrating updatable security barrier view support with row-security,
it has become a lot harder to detect and prevent infinite recursion
(including mutual recursion) in row-security policy expansion.The code is attached, but it's not an independent patch so it's way
easier to grab it from git:git@github.com:ringerc/postgres.git
branch rls-9.4-upd-sb-views (subject to rebase); or
tag rls-9.4-upd-sb-views-v1(Only contains half the old row-security patch; I'll rebase the docs,
tests, etc on top of this once it's working properly).I've integrated the updatable security barrier view support into RLS by
injecting securityQuals in subquery_planner() just before
preprocess_rowmarks . (I'm still thinking about some inheritance related
aspects to that, but that's for later).The problem is that this causes infinite recursion - the securityQuals
get expanded into a subquery over the original RTE that had the
row-security policy on it. Then subquery_planner is re-entered when
processing the subquery, a row-security qual is found on the target RTE,
and ... *boom*.Fixing this is not as simple as suppressing expansion of row-security
policy when processing a security barrier subquery created by a
row-security policy, as it is desirable to respect the row-security
policy of *other* tables that get referenced in the expanded
row-security qual.If we just record the relid of the relation a subquery was expanded from
and avoid expanding that inside the generated subquery we prevent simple
linear recursion, but not mutual recursion between two or more rels with
row-security policy.KaiGai's row-security patch handles this because it does its own
recursive expansion, so (like the rewriter) it can keep a breadcrumb
trail and detect when it is repeating a path. That's not so practical
when row-security code tags RTEs with policy, then updatable s.b. views
goes and expands them.So. Options.
1.Come up with a reasonable way to track recursive row-security
expansion, detect infinite recursion, and bail out. Likely to involve
a new global structure with planner/optimizer lifetime that gets
checked and maintained by apply_row_security_rangetbl.2.Solve the linear recursion case by storing a relid that should not get
expanded for security quals when processing a subquery. Say "don't do
that, expect stack exceeded crashes if you do" for the mutual
recursion case. Seems user hostile, incomplete, and likely to break
people's systems when they really don't expect it.3.Disregard row-security policy on referenced tables when expanding
row-security qualifiers. There's precendent here in foreign keys,
which ignore row-security policy, but I don't think this is at all
appealing.4.Magic?
My first thought is to add a boolean flag to RangeTblEntry (similar to
the "inh" flag) to say whether RLS expansion is requested for that
RTE. Then set it to false on each RTE as you add new RLS quals to it's
securityQuals.
In addition, when adding RLS quals to an RTE, I think they should be
fully and recursively expanded immediately, before setting the new
flag to false and moving on --- think recursively calling the rewriter
to expand view references in the new RLS qual, and
expand_security_qual() to expand any additional RLS quals in the
securityQuals list --- with loop detection there. I'm not pretending
that's going to be easy, but there are a couple of existing pieces of
code to borrow ideas from. Doing it this way should make it possible
to do the loop detection without any global structures.
Regards,
Dean
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 01/24/2014 07:16 PM, Dean Rasheed wrote:
My first thought is to add a boolean flag to RangeTblEntry (similar to
the "inh" flag) to say whether RLS expansion is requested for that
RTE. Then set it to false on each RTE as you add new RLS quals to it's
securityQuals.
That's what I was getting at with adding flags to RangeTblEntry, yes.
Given the number of flags we're growing I wonder if they should be
consolodated into a bitmask, but I'll leave that problem for later.
I'll do this part first, since it seems you agree that a RangeTblEntry
flag is the appropriate path. That'll make row-security checking work
and make the patch testable.
It won't deal with recursive rules, they'll still crash the backend.
I'll deal with that as a further step.
In addition, when adding RLS quals to an RTE, I think they should be
fully and recursively expanded immediately, before setting the new
flag to false and moving on --- think recursively calling the rewriter
to expand view references in the new RLS qual, and
expand_security_qual() to expand any additional RLS quals in the
securityQuals list --- with loop detection there. I'm not pretending
that's going to be easy, but there are a couple of existing pieces of
code to borrow ideas from. Doing it this way should make it possible
to do the loop detection without any global structures.
Ugh. I was really hoping to avoid *another* place where we do recursive
expansion and infinite recursion checking, especially when the rewriter
already does this job.
Unfortunately, so long as the rewriter doesn't know about inheritance,
it cannot fully solve this problem. A mutually recursive rule involving
inheritance wouldn't get detected by rewriter based checking.
The original RLS patch only detects direct recursion, btw, it looks like
it won't catch mutual recursion.
--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 01/24/2014 07:16 PM, Dean Rasheed wrote:
think recursively calling the rewriter
to expand view references in the new RLS qual, and
expand_security_qual() to expand any additional RLS quals in the
securityQuals list
With this, it'd be helpful if expand_security_qual(...) took a
RangeTblEntry instead of an rt_index.
That'd also be much more efficient with large rtables if we can arrange
a scan through the rtable when looking for security quals.
Like other places that operate on the rangetable while it's being
modified, we can walk the rangetable list up until the final entry that
existed when we started walking. This approach saves the series of
rt_fetch calls, which are something like O(n log n) for n relations.
It's safe because the operation will only append rangetable entries.
(I can't help wonder how much we'd gain by making the rtable an array
that gets doubled in size and copied whenever it overflows, rather than
a linked list, given all the walking of it that gets done, and how dead
entries to get flagged as dead rather than actually removed.)
I'm looking for where I found the code that already does this so I can
point and say "I'm not crazy, we already do it here". Will follow up
with a patch.
--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 01/28/2014 04:39 PM, Craig Ringer wrote:
I'm looking for where I found the code that already does this so I can
point and say "I'm not crazy, we already do it here". Will follow up
with a patch.
I spoke to soon. The code I'm talking about is
expand_inherited_tables(...) and it still uses rt_fetch, it just avoids
foreach(...) in favour of stopping scanning at the end of the original
rtable.
So I get to be crazy after all.
I really don't like how many places we're rt_fetch'ing the same RTE from
in updatable s.b. views and its interaction with row-security, but that
can be a "later" problem. For now I'll stick with RTIs.
--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 01/28/2014 02:11 PM, Craig Ringer wrote:
My first thought is to add a boolean flag to RangeTblEntry (similar to
the "inh" flag) to say whether RLS expansion is requested for that
RTE. Then set it to false on each RTE as you add new RLS quals to it's
securityQuals.That's what I was getting at with adding flags to RangeTblEntry, yes.
Given the number of flags we're growing I wonder if they should be
consolodated into a bitmask, but I'll leave that problem for later.I'll do this part first, since it seems you agree that a RangeTblEntry
flag is the appropriate path. That'll make row-security checking work
and make the patch testable.It won't deal with recursive rules, they'll still crash the backend.
I'll deal with that as a further step.
I've put together a working RLS patch on top of updatable security
barrier views.
It has some known issues remaining; it doesn't do recursion checking
yet, and it fails some of the regression tests in exciting ways. I'm
looking into them step by step.
Some differences in the tests behaviours that have changed due to the
inheritance rules changing; many appear to be oversights or bugs yet to
be chased down.
You can find it here;
https://github.com/ringerc/postgres/compare/rls-9.4-upd-sb-views
i.e. https://github.com/ringerc/postgres.git ,
branch rls-9.4-upd-sb-views
(subject to rebasing) or the non-rebased tag rls-9.4-upd-sb-views-v2
The guts of the patch appear as a diff, attached, but it's not
standalone so I suggest using git.
I'll be looking into recursion issues and the test failures tomorrow.
--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
Attachments:
0001-RLS-Add-rowsec_done-flag-to-RangeTblEntry.patchtext/x-patch; name=0001-RLS-Add-rowsec_done-flag-to-RangeTblEntry.patchDownload
>From 03c02389c7b475d06864f9a7f38fd583c6e891e9 Mon Sep 17 00:00:00 2001
From: Craig Ringer <craig@2ndquadrant.com>
Date: Tue, 28 Jan 2014 14:23:23 +0800
Subject: [PATCH 1/4] RLS: Add rowsec_done flag to RangeTblEntry
To be used to track completion status of row security expansion on a
range table entry. Rels with this flag set must not be row-security
expanded.
---
src/backend/nodes/copyfuncs.c | 1 +
src/backend/nodes/outfuncs.c | 1 +
src/backend/nodes/readfuncs.c | 1 +
src/include/nodes/parsenodes.h | 1 +
4 files changed, 4 insertions(+)
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 3e2acf0..9607911 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -1975,6 +1975,7 @@ _copyRangeTblEntry(const RangeTblEntry *from)
COPY_SCALAR_FIELD(rtekind);
COPY_SCALAR_FIELD(relid);
COPY_SCALAR_FIELD(relkind);
+ COPY_SCALAR_FIELD(rowsec_done);
COPY_NODE_FIELD(subquery);
COPY_SCALAR_FIELD(security_barrier);
COPY_SCALAR_FIELD(rowsec_relid);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index dd447ed..974d169 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -2372,6 +2372,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node)
case RTE_RELATION:
WRITE_OID_FIELD(relid);
WRITE_CHAR_FIELD(relkind);
+ WRITE_BOOL_FIELD(rowsec_done);
break;
case RTE_SUBQUERY:
WRITE_NODE_FIELD(subquery);
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index a0cb369..7a43b59 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -1214,6 +1214,7 @@ _readRangeTblEntry(void)
case RTE_RELATION:
READ_OID_FIELD(relid);
READ_CHAR_FIELD(relkind);
+ READ_BOOL_FIELD(rowsec_done);
break;
case RTE_SUBQUERY:
READ_NODE_FIELD(subquery);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index b3e718a..b3ae722 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -736,6 +736,7 @@ typedef struct RangeTblEntry
*/
Oid relid; /* OID of the relation */
char relkind; /* relation kind (see pg_class.relkind) */
+ bool rowsec_done; /* True if row-security quals checked for and applied already */
/*
* Fields valid for a subquery RTE (else NULL):
--
1.8.3.1
0002-RLS-Enforce-row-security-constraints.patchtext/x-patch; name=0002-RLS-Enforce-row-security-constraints.patchDownload
>From b0f5797d81a4b856f26818e14c93a0ec453a35de Mon Sep 17 00:00:00 2001
From: Craig Ringer <craig@2ndquadrant.com>
Date: Fri, 24 Jan 2014 16:18:40 +0800
Subject: [PATCH 2/4] RLS: Enforce row-security constraints
Row-security constraints are enforced here by having the securityQual expansion
code check for a row-security constraint before expanding quals.
---
src/backend/optimizer/prep/prepsecurity.c | 17 +++-
src/backend/optimizer/prep/prepunion.c | 5 ++
src/backend/optimizer/util/Makefile | 3 +-
src/backend/optimizer/util/rowsecurity.c | 144 ++++++++++++++++++++++++++++++
src/include/optimizer/rowsecurity.h | 25 ++++++
5 files changed, 192 insertions(+), 2 deletions(-)
create mode 100644 src/backend/optimizer/util/rowsecurity.c
create mode 100644 src/include/optimizer/rowsecurity.h
diff --git a/src/backend/optimizer/prep/prepsecurity.c b/src/backend/optimizer/prep/prepsecurity.c
index e7a3f56..ad682bb 100644
--- a/src/backend/optimizer/prep/prepsecurity.c
+++ b/src/backend/optimizer/prep/prepsecurity.c
@@ -1,7 +1,8 @@
/*-------------------------------------------------------------------------
*
* prepsecurity.c
- * Routines for preprocessing security barrier quals.
+ * Routines for preprocessing security barrier quals and applying row-security
+ * policies.
*
* Portions Copyright (c) 1996-2013, PostgreSQL Global Development Group
* Portions Copyright (c) 1994, Regents of the University of California
@@ -15,6 +16,7 @@
#include "postgres.h"
#include "access/heapam.h"
+#include "access/transam.h"
#include "access/sysattr.h"
#include "catalog/heap.h"
#include "nodes/makefuncs.h"
@@ -23,6 +25,7 @@
#include "parser/parsetree.h"
#include "rewrite/rewriteManip.h"
#include "utils/rel.h"
+#include "optimizer/rowsecurity.h"
typedef struct
@@ -75,6 +78,18 @@ expand_security_quals(PlannerInfo *root, List *tlist)
rt_index++;
rte = rt_fetch(rt_index, parse->rtable);
+ /*
+ * Check for row-security quals on the relation and, if found, prepend them
+ * as new inner-most security quals. Ignore the return, we don't care at the
+ * moment.
+ *
+ * This will set rowsec_done on the RTE, which we'll copy if we expand
+ * it, ensuring that no infinitely recursive expansion of row security quals
+ * is done.
+ */
+ (void) prepend_row_security_quals(root, rte);
+
+
if (rte->securityQuals == NIL)
continue;
diff --git a/src/backend/optimizer/prep/prepunion.c b/src/backend/optimizer/prep/prepunion.c
index c7e0199..0ae60c1 100644
--- a/src/backend/optimizer/prep/prepunion.c
+++ b/src/backend/optimizer/prep/prepunion.c
@@ -1349,6 +1349,11 @@ expand_inherited_rtentry(PlannerInfo *root, RangeTblEntry *rte, Index rti)
childRTindex = list_length(parse->rtable);
/*
+ * XXX TODO We need to apply row-security policy after inheritance
+ * expansion.
+ */
+
+ /*
* Build an AppendRelInfo for this parent and child.
*/
appinfo = makeNode(AppendRelInfo);
diff --git a/src/backend/optimizer/util/Makefile b/src/backend/optimizer/util/Makefile
index c54d0a6..d42a285 100644
--- a/src/backend/optimizer/util/Makefile
+++ b/src/backend/optimizer/util/Makefile
@@ -13,6 +13,7 @@ top_builddir = ../../../..
include $(top_builddir)/src/Makefile.global
OBJS = clauses.o joininfo.o orclauses.o pathnode.o placeholder.o \
- plancat.o predtest.o relnode.o restrictinfo.o tlist.o var.o
+ plancat.o predtest.o relnode.o restrictinfo.o tlist.o var.o \
+ rowsecurity.o
include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/optimizer/util/rowsecurity.c b/src/backend/optimizer/util/rowsecurity.c
new file mode 100644
index 0000000..3f26d2f
--- /dev/null
+++ b/src/backend/optimizer/util/rowsecurity.c
@@ -0,0 +1,144 @@
+/*
+ * rewrite/rowsecurity.c
+ * Routines to support row-security feature
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ */
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "access/htup_details.h"
+#include "access/sysattr.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_inherits_fn.h"
+#include "catalog/pg_rowsecurity.h"
+#include "catalog/pg_type.h"
+#include "miscadmin.h"
+#include "nodes/makefuncs.h"
+#include "nodes/nodeFuncs.h"
+#include "nodes/plannodes.h"
+#include "optimizer/clauses.h"
+#include "optimizer/prep.h"
+#include "optimizer/rowsecurity.h"
+#include "parser/parsetree.h"
+#include "rewrite/rewriteHandler.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "tcop/utility.h"
+
+/* hook to allow extensions to apply their own security policy */
+row_security_policy_hook_type row_security_policy_hook = NULL;
+
+static void
+add_uid_plan_inval(PlannerGlobal* glob);
+
+static List *
+pull_row_security_policy(CmdType cmd, Relation relation);
+
+/*
+ * Check the given RTE to see whether it's already had row-security
+ * quals expanded and, if not, prepend any row-security rules
+ * from built-in or plug-in sources to the securityQuals.
+ *
+ * Returns true if any quals were added.
+ */
+bool
+prepend_row_security_quals(PlannerInfo* root, RangeTblEntry* rte)
+{
+ List *rowsecquals;
+ Relation rel;
+ Oid userid;
+ int sec_context;
+
+ GetUserIdAndSecContext(&userid, &sec_context);
+
+ bool qualsAdded = false;
+ if (rte->relid >= FirstNormalObjectId
+ && rte->relkind == 'r'
+ && !rte->rowsec_done
+ && !(sec_context & SECURITY_ROW_LEVEL_DISABLED))
+ {
+ rel = heap_open(rte->relid, NoLock);
+ rowsecquals = pull_row_security_policy(root->parse->commandType, rel);
+ if (rowsecquals)
+ {
+ rte->securityQuals = list_concat(rowsecquals, rte->securityQuals);
+ qualsAdded = true;
+ }
+ heap_close(rel, NoLock);
+ rte->rowsec_done = true;
+ }
+ if (qualsAdded)
+ add_uid_plan_inval(root->glob);
+ return qualsAdded;
+}
+
+/*
+ * pull_row_security_policy
+ *
+ * Fetches the configured row-security policy of both built-in catalogs and any
+ * extensions. If any policy is found a list of qualifier expressions is
+ * returned, where each is treated as a securityQual.
+ */
+static List *
+pull_row_security_policy(CmdType cmd, Relation relation)
+{
+ List *quals = NIL;
+ Expr *qual = NULL;
+
+ /*
+ * Pull the row-security policy configured with built-in features,
+ * if unprivileged users. Please note that superuser can bypass it.
+ */
+ if (relation->rsdesc && !superuser())
+ {
+ RowSecurityDesc *rsdesc = relation->rsdesc;
+ qual = copyObject(rsdesc->rsall.qual);
+ quals = lcons(qual, quals);
+ }
+
+ /*
+ * Also, ask extensions whether they want to apply their own
+ * row-security policy. If both built-in and extension has
+ * their own policy they're applied as nested qualifiers.
+ */
+ if (row_security_policy_hook)
+ {
+ List *temp;
+
+ temp = (*row_security_policy_hook)(cmd, relation);
+ if (temp != NIL)
+ lcons(temp, quals);
+ }
+ return quals;
+}
+
+/*
+ * Row-security plans are dependent on the current user id because of the if
+ * (superuser) test. So row-security plans must be invalidated if the user id
+ * changes.
+ */
+static void
+add_uid_plan_inval(PlannerGlobal* glob)
+{
+ PlanInvalItem *pi_item;
+
+ if (!OidIsValid(glob->planUserId))
+ {
+ /* Plan invalidation on session user-id */
+ glob->planUserId = GetUserId();
+
+ /* Plan invalidation on catalog updates of pg_authid */
+ pi_item = makeNode(PlanInvalItem);
+ pi_item->cacheId = AUTHOID;
+ pi_item->hashValue =
+ GetSysCacheHashValue1(AUTHOID,
+ ObjectIdGetDatum(glob->planUserId));
+ glob->invalItems = lappend(glob->invalItems, pi_item);
+ }
+ else
+ Assert(glob->planUserId == GetUserId());
+
+}
diff --git a/src/include/optimizer/rowsecurity.h b/src/include/optimizer/rowsecurity.h
new file mode 100644
index 0000000..453d79a
--- /dev/null
+++ b/src/include/optimizer/rowsecurity.h
@@ -0,0 +1,25 @@
+/* -------------------------------------------------------------------------
+ *
+ * rowsecurity.h
+ * prototypes for optimizer/rowsecurity.c
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * -------------------------------------------------------------------------
+ */
+#ifndef ROWSECURITY_H
+#define ROWSECURITY_H
+
+#include "nodes/execnodes.h"
+#include "nodes/parsenodes.h"
+#include "nodes/relation.h"
+#include "utils/rel.h"
+
+typedef List *(*row_security_policy_hook_type)(CmdType cmdtype,
+ Relation relation);
+extern PGDLLIMPORT row_security_policy_hook_type row_security_policy_hook;
+
+extern bool prepend_row_security_quals(PlannerInfo* root, RangeTblEntry* rte);
+
+#endif /* ROWSECURITY_H */
--
1.8.3.1
On 01/29/2014 09:47 PM, Craig Ringer wrote:
https://github.com/ringerc/postgres/compare/rls-9.4-upd-sb-views
i.e. https://github.com/ringerc/postgres.git ,
branch rls-9.4-upd-sb-views(subject to rebasing) or the non-rebased tag rls-9.4-upd-sb-views-v2
Pushed an update to the branch. New update tagged
rls-9.4-upd-sb-views-v3 . Fixes an issue with rowmarking that stems from
the underlying updatable s.b. views patch.
Other tests continue to fail, this isn't ready yet.
--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 01/30/2014 01:25 PM, Craig Ringer wrote:
On 01/29/2014 09:47 PM, Craig Ringer wrote:
https://github.com/ringerc/postgres/compare/rls-9.4-upd-sb-views
i.e. https://github.com/ringerc/postgres.git ,
branch rls-9.4-upd-sb-views(subject to rebasing) or the non-rebased tag rls-9.4-upd-sb-views-v2
Pushed an update to the branch. New update tagged
rls-9.4-upd-sb-views-v3 . Fixes an issue with rowmarking that stems from
the underlying updatable s.b. views patch.Other tests continue to fail, this isn't ready yet.
Specifically:
- Needs checks in AT INHERITS, AT SET ROW SECURITY, and CT INHERITS to
prohibit any combination of inheritance and row-security, per:
/messages/by-id/52EA01C3.70804@2ndquadrant.com
- row-security rule recursion detection isn't solved yet, it just
overflows the stack.
- COPY doesn't know anything about row-security
- I'm just starting to chase some odd errors in the tests, "ERROR:
failed to find unique expression in subplan tlist" and "ERROR: could
not open file "base/16384/30070": No such file or directory". Their
cause/origin is not yet known, but they're specific to when row-security
policy is being applied.
- policies based on current_user don't "remember" current_user when rows
are pulled from refcursor returned by a security definer function.
There is a chunk of work here. Anybody who wants row-security to happen
for 9.4, please pick something and pitch in.
(Or we could just decide that my rebased and tweaked version of KaiGai's
original patch internal query structure twiddling aside, is the best way
forward after all. That leaves only the last item to deal with.)
--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 01/30/2014 04:05 PM, Craig Ringer wrote:
On 01/30/2014 01:25 PM, Craig Ringer wrote:
On 01/29/2014 09:47 PM, Craig Ringer wrote:
https://github.com/ringerc/postgres/compare/rls-9.4-upd-sb-views
i.e. https://github.com/ringerc/postgres.git ,
branch rls-9.4-upd-sb-views(subject to rebasing) or the non-rebased tag rls-9.4-upd-sb-views-v2
Pushed an update to the branch. New update tagged
rls-9.4-upd-sb-views-v3 . Fixes an issue with rowmarking that stems from
the underlying updatable s.b. views patch.Other tests continue to fail, this isn't ready yet.
Specifically:
- row-security rule recursion detection isn't solved yet, it just
overflows the stack.
This is now fixed in the newly tagged rls-9.4-upd-sb-views-v4 in
git@github.com:ringerc/postgres.git .
I landed up adding a field to RangeTblEntry that keeps track of all the
oids of relations row-security expanded to produce this RTE. When
testing an RTE for row-security policy, this list is checked to see if
the oid of the relation being expanded is already on the list. The list
is copied to all RTEs inside sublinks after a relation is expanded using
a query_tree_walker.
- COPY doesn't know anything about row-security
COPY will ERROR on row-security rels in rls-9.4-upd-sb-views-v4.
I'm looking at integrating the approach Kohei KaiGai took in the
original patch now, then I'll be moving on to:
- I'm just starting to chase some odd errors in the tests, "ERROR:
failed to find unique expression in subplan tlist" and "ERROR: could
not open file "base/16384/30070": No such file or directory". Their
cause/origin is not yet known, but they're specific to when row-security
policy is being applied.
I was hoping these would be fixed by solving the recursion issues, but
that wasn not the case.
Input/comments would be appreciated. I haven't looked into this yet.
- policies based on current_user don't "remember" current_user when rows
are pulled from refcursor returned by a security definer function.
This is actually a separate, existing bug, or surprising behaviour. I'd
like to address it, but it's really a separate patch.
--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Craig Ringer <craig@2ndquadrant.com> writes:
I landed up adding a field to RangeTblEntry that keeps track of all the
oids of relations row-security expanded to produce this RTE. When
testing an RTE for row-security policy, this list is checked to see if
the oid of the relation being expanded is already on the list. The list
is copied to all RTEs inside sublinks after a relation is expanded using
a query_tree_walker.
That's impossibly ugly, both as to the idea that an RTE is a mutable
data structure for this processing, and as to having to copy the list
around (meaning that you can't even claim to have one source of truth
for the state...)
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 02/04/2014 03:14 PM, Tom Lane wrote:
Craig Ringer <craig@2ndquadrant.com> writes:
I landed up adding a field to RangeTblEntry that keeps track of all the
oids of relations row-security expanded to produce this RTE. When
testing an RTE for row-security policy, this list is checked to see if
the oid of the relation being expanded is already on the list. The list
is copied to all RTEs inside sublinks after a relation is expanded using
a query_tree_walker.That's impossibly ugly, both as to the idea that an RTE is a mutable
data structure for this processing, and as to having to copy the list
around (meaning that you can't even claim to have one source of truth
for the state...)
I see. Do you have any suggestion about how this might be approached in
a manner that would be more satisfactory?
It's not strictly necessary to duplicate the parent list when annotating
the RTEs in an expanded row-security qual; I did so only out of concern
that an object of indeterminate/shared ownership may not be considered
desirable.
The only time it's strictly neccessary to copy the parent list is when a
new row-security qual is being expanded. In this case it cannot be
shared, because there may be multiple relations with row-security
applied, and a separate "path" is required for each. If the list is
shared, infinite recursion is falsely reported in cases like:
rel a has a qual referring to b
rel b has a qual referring to c
rel c has a qual referring to d
select ...
from a inner join b on ...
There's no infinite recursion there, but a simple approach that keeps a
global list of expanded quals will think there is as it'll see "b" and
"c" expanded twice.
So whenever you expand a qual, you have to "fork" the parent list,
copying it.
The approach used in the rewriter won't work here, because the rewriter
eagerly depth-first recurses when expanding things. In the optimizer,
all securityQuals get expanded in one pass, _then_ sublink processing
expands any added sublinks in a separate pass.
It's possible the parent list could be associated with the Query that's
produced by securityQual expansion instead, but at the time you're
expanding row-security on any RTEs within that you don't necessarily
have access to the Query node that might be a couple of subquery levels
up, since the security qual can be an arbitrary expression.
I looked at keeping a structure in PlannerGlobal that tracked the chain
of row-security qual expansion, but I couldn't figure out a way to
cleanly tell what "branch" of the query a given RTE that's being
expanded was in, and thus how to walk the global tree to check for
infinite recursion.
The only alternative I see is to immediately and recursively expand
row-security entries within subqueries using a walker, as soon as the
initial row-security qual is expanded on the top level rel.
I'm concerned about code duplication when following that path, since
that's pretty much what's already happening with sublink expansion, just
using the existing execution paths.
If I'm right, and immediate recursive expansion isn't an acceptable
alternative, any ideas on how the row-security expansion breadcrumb
trail might be stored in a more appropriate manner and accessed from
where they're needed?
Note that it's trivial to prevent simple, direct recursion. Preventing
mutual recursion without also breaking non-recursive cases where rels
are used in totally different parts of the query is harder.
--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 02/04/2014 02:43 PM, Craig Ringer wrote:
On 01/30/2014 04:05 PM, Craig Ringer wrote:
On 01/30/2014 01:25 PM, Craig Ringer wrote:
On 01/29/2014 09:47 PM, Craig Ringer wrote:
https://github.com/ringerc/postgres/compare/rls-9.4-upd-sb-views
i.e. https://github.com/ringerc/postgres.git ,
branch rls-9.4-upd-sb-views(subject to rebasing) or the non-rebased tag rls-9.4-upd-sb-views-v2
Pushed an update to the branch. New update tagged
rls-9.4-upd-sb-views-v3 . Fixes an issue with rowmarking that stems from
the underlying updatable s.b. views patch.Other tests continue to fail, this isn't ready yet.
Specifically:
- row-security rule recursion detection isn't solved yet, it just
overflows the stack.This is now fixed in the newly tagged rls-9.4-upd-sb-views-v4 in
git@github.com:ringerc/postgres.git .
Based on Tom's objections, another approach is presented in
rls-9.4-upd-sb-views-v5 on git@github.com:ringerc/postgres.git . The
Query node is used to record the recursive expansion parent list
instead, and copying is avoided.
However, I've separately tracked down the cause of the test failures like:
ERROR: could not open file "base/16384/30135": No such file or directory
This occurs where a row-security qual is declared to use a view.
Row-security quals get stored without rewriting (which is necessary, see
below). The qual is injected into securityQuals and expanded, but *not
rewritten*. So the executor sees an unexpected view in the tree.
Because a view RTE has its relid field set to the view's oid, this
doesn't get picked up until we try to actually scan the view relation in
the executor.
(I'd like to add Asserts to make the executor fail a bit more
informatively when you try to scan a view, but that's separate.)
So, that's clearly broken. There are really two possible solutions:
1. Try (again) to do row-security in the rewriter. This was previously
impossible because of the definition of row-security behaviour around
inheritance, but with the simplified inheritance model now proposed I
think it's possible.
2. Invoke RewriteQuery from within expand_security_quals, rewriting the
query after security qual expansion. This is only needed for
row-security; for updatable s.b. views rewriting has happened with
recursion into securityQuals during the original rewrite pass.
I suspect that (2) will result in a resounding "yuck".
So I have to see if I can now turn around *again* and plug row-security
into the rewriter after all. That's a pretty frustrating thing to
discover in mid-late CF4.
--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 2014-02-06 05:43, Craig Ringer wrote:
Based on Tom's objections, another approach is presented in
rls-9.4-upd-sb-views-v5 on git@github.com:ringerc/postgres.git . The
Query node is used to record the recursive expansion parent list
instead, and copying is avoided.
Cannot fetch or clone.
github on web says "This repository is temporarily unavailable. The
backend storage is temporarily offline. Usually this means the storage
server is undergoing maintenance. Please contact support if the problem
persists."
regards,
Yeb
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 02/06/2014 04:54 PM, Yeb Havinga wrote:
On 2014-02-06 05:43, Craig Ringer wrote:
Based on Tom's objections, another approach is presented in
rls-9.4-upd-sb-views-v5 on git@github.com:ringerc/postgres.git . The
Query node is used to record the recursive expansion parent list
instead, and copying is avoided.Cannot fetch or clone.
github on web says "This repository is temporarily unavailable. The
backend storage is temporarily offline. Usually this means the storage
server is undergoing maintenance. Please contact support if the problem
persists."
If this persists, or someone else has the same issue, try the HTTPS url:
https://github.com/ringerc/postgres.git
per: http://github.com/ringerc/postgres/
(I need to chase up my git.postgresql.org account request, it seems to
have got lost).
--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 02/06/2014 12:43 PM, Craig Ringer wrote:
1. Try (again) to do row-security in the rewriter. This was previously
impossible because of the definition of row-security behaviour around
inheritance, but with the simplified inheritance model now proposed I
think it's possible.
Thanks to the simplified requirements for inheritance, this turns out to
be fairly easy. There's a version rewritten to use the rewriter in the tag:
rls-9.4-upd-sb-views-v6
on https://github.com/ringerc/postgres.git
The trickiest bit remaining is how to register the PlanInvalItem to
force plan invalidation when the user-id changes. This was easy in the
optimizer, but it's not obvious how to do it cleanly in the rewriter.
I've got a couple of ideas but don't much like either of them.
Recommendations from the experienced welcomed.
Other more minor open items, each of which look quite quick:
I haven't plugged in rewriter-based recursion detection yet, but that
looks fairly easy (famous last words?). It's 10pm and I have an early
start, so that won't happen tonight.
I haven't removed some cruft from the previous approach from the Query
node either; I need to trim the diff.
The regression test expected file needs adjustment to match the new
inheritance rules, and to cover a couple of new tests I've added.
Needs a better error when it gets an unacceptable rewrite product during
security qual rewriting (modeled on the errors for CTEs). Easy, just
some cookie cutter code.
Need to cherry-pick the docs patch back on top of the patch tree now
things are settling.
Amazingly, I think this has a hope.
--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 02/06/2014 10:19 PM, Craig Ringer wrote:
On 02/06/2014 12:43 PM, Craig Ringer wrote:
1. Try (again) to do row-security in the rewriter. This was previously
impossible because of the definition of row-security behaviour around
inheritance, but with the simplified inheritance model now proposed I
think it's possible.Thanks to the simplified requirements for inheritance, this turns out to
be fairly easy. There's a version rewritten to use the rewriter in the tag:rls-9.4-upd-sb-views-v6
on https://github.com/ringerc/postgres.git
The trickiest bit remaining is how to register the PlanInvalItem to
force plan invalidation when the user-id changes. This was easy in the
optimizer, but it's not obvious how to do it cleanly in the rewriter.
I've got a couple of ideas but don't much like either of them.
Recommendations from the experienced welcomed.
Or, after thinking about it for a second with my tired brain, "not so much".
We don't rerun rewrite on plan invalidation.
So that means the superuser exemption won't work properly with this patch.
So much for having a hope, that's not a small thing to fix.
So: either I invoke the rewriter from within the optimizer on the
security quals, or I make the rewriter re-run on plan invalidation.
Neither is small or simple.
Blast.
--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Craig Ringer <craig@2ndquadrant.com> writes:
We don't rerun rewrite on plan invalidation.
Don't we? plancache.c certainly does, in fact it starts from the raw
grammar output. Skipping the rewriter would mean failing to respond
to CREATE OR REPLACE VIEW, for example.
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 02/06/2014 11:11 PM, Tom Lane wrote:
Craig Ringer <craig@2ndquadrant.com> writes:
We don't rerun rewrite on plan invalidation.
Don't we? plancache.c certainly does, in fact it starts from the raw
grammar output. Skipping the rewriter would mean failing to respond
to CREATE OR REPLACE VIEW, for example.
I was thinking about exactly that case as I went to sleep - especially
as it's things like CREATE OR REPLACE VIEW that prevent me from just
rewriting the security qual when it's initially added, before storage in
the catalogs.
I could've sworn discussion around row security in the past concluded
that it couldn't be properly done in the rewriter because of an
inability to correctly invalidate plans. Searching the archives, though,
I don't find anything to support that. I'll just say I'm glad to be
wrong, and proceed from there.
--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 06/02/14 15:19, Craig Ringer wrote:
Thanks to the simplified requirements for inheritance, this turns out to
be fairly easy. There's a version rewritten to use the rewriter in the tag:rls-9.4-upd-sb-views-v6
Hi Craig, list,
This is review of the current RLS patch on a database schema that uses
inheritance in the 'classical' sense (not partitioning). The review was
done on rls-9.4-upd-sb-views-v4 and hence all comments are about that
version. Comparing output of the minisql script between v4 and v6 gives
differences, as v6 seems to be WIP.
Our goal is to implement the HL7 Reference Information Model (RIM) in
PostgreSQL. A fine-grained access control on the tables would have a
practical use in the context of RIM. So, we have made some preliminary
tests of the Row Security patch for such a specific data model. For the
purpose of reviewing RLS, we have restricted the full RIM to just a few
tables which we call the mini-RIM. It is important to observe that the
RIM uses inheritance, and we use PostgreSQL inheritance to implement the
RIM's inheritance. More details about the RIM are presented below.
In the attached SQL script we list a mini-RIM, along with examples of
RLS enforcement.
General comments about RLS applied on (a minimalistic version of) the
RIM can be summarized as follows:
1. The current RLS implementation works for use cases where
confidentiality attributes are specified in the inheritance root
relation. Since security labelling in the RIM is done on
confidentialitycodes that are present in the inheritance roots (e.g.,
Role and Act), the current RLS works for the RIM.
2. Infinite recursion is well captured in case of recursive restrictions
to tables.
3. RLS syntax is readable and easy to use.
4. Documentation needs work.
5. Subqueries in RLS quals can be pulled up, so opens the ability for
fast processing.
Overall from a users perspective the patch gave a solid impression.
regards,
Yeb Havinga
Albana Gaba
Henk-Jan Meijer
Portavita B.V. The Netherlands
BACKGROUND ON THE REFERENCE INFORMATION MODEL:
To understand how The HL7 Reference Information Model (RIM) uses
PostgreSQL inheritance, it is helpful to understand the meaning of the
content of the parent and child tables. This section describes the
background of the RIM, and describes a few classes of the �Act� hierarchy.
The HL7 RIM[1]�HL7 Reference Information Model� http://www.hl7.org/implement/standards/rim.cfm is not just yet another information model. It is a
mature, standard information model that has been used and refined over
the course of many years [2]� Influences of the Unified Service Action Model on the HL7 Reference Information Model.� https://www.ncbi.nlm.nih.gov/pmc/articles/PMC2232835/[3]History of the HL7 RIM http://www.ringholm.de/docs/04500_en_History_of_the_HL7_RIM.htm. Its purpose is to capture detailed
information for medical records. Pivotal in the RIM is its action-based
modelling, based on ideas that can be traced back to the American
philosopher C.S. Peirce. A direct line from Peirce�s Logic of Relatives
to the foundations of relational databases has been introduced in [4]�Charles Peirce� http://www.newworldencyclopedia.org/entry/Charles_Peirce.
Versions of the RIM are now being released as an ANSI standard.
An illustration of the RIM is available at
http://www.healthintersections.com.au/wp-content/uploads/2011/05/RIM.png
The RIM is a set of UML classes, each containing one or more attributes.
The classes are an abstraction of subjects or other concepts that are
relevant within the healthcare domain. To avoid a model with a huge
number of classes, the RIM defines six core classes whereas the vast
majority of the classes are defined as specializations based on the core
ones. The specialization classes inherit all the properties of the
generalization classes while adding specific attributes of its own. To
make matters concrete, let us look at "Act" class.
�Act�: Looking at the right hand side of the RIM illustration referenced
above, we can see the class �Act� and its specializations, and this is
the focal point for the RIM�s action based modeling. Description from
the standard: �Acts are the pivot of the RIM: domain information and
process records are represented primarily in Acts. Any profession or
business, including healthcare, is primarily constituted of intentional
and occasionally non-intentional actions, performed and recorded by
responsible actors. An Act-instance is a record of such an action.�
Notable attributes of �Act� are:
�id� - A unique identifier for the Act. Each Act is associated with a
unique id. All specialization of Act inherit this id. This means that if
there is, for example, an instance of Observation with id 5, there exist
no other acts with id 5. In fact, since technically in the RIM all
identifiers stem from a single infrastructure root, the identifiers are
globally unique: there exists a single object with id 5. This single
object is an instance of Observation, and since Observation is a
specialization of Act, it is also an instance of Act.
�classCode� � The major class of Acts to which an Act-instance belongs.
The allowed codes in classCode form a hierarchical code system. In the
2011 RIM, there are 124 different class codes. This is a larger number
than the number of specializations in the class diagram: only the
classes that need additional properties have their own class definition
in the diagram.
�moodCode� - The intended use of the Act statement: as a report of fact,
a command, a possibility, a goal, etc.
�code� - The particular kind of Act that the Act-instance represents
within its class.
�confidentialityCode� - Codes that identify how sensitive a piece of
information is and/or that indicate how the information may be made
available or disclosed.
Notable specializations of �Act� are �Observation� and
�SubstanceAdministration�.:
�Observation� - The main difference between Observations and other Acts
is that Observations have a value attribute. The code attribute of
Observation and the value attribute of Observation must be considered in
combination to determine the semantics of the observation. Structurally,
many observations are name-value-pairs, where the Observation.code
(inherited from Act) is the name and the Observation.value is the value
of the property. Such a construct is also known as a "variable" (a named
feature that can assume a value); hence, the Observation class is always
used to hold generic name-value-pairs or variables, even though the
variable valuation may not be the result of an elaborate observation
method. It may be a simple answer to a question or it may be an
assertion or setting of a parameter.
�SubstanceAdministration� - A type of procedure that involves a
performer introducing or otherwise applying a material into or to the
subject. Substance administration is distinguished from an exposure by
the participation of a performer in the act. The substance administered
by a performer physically interacts with the subject or is otherwise
"taken in" by the subject during the act of administration. Detailed
information about the supplied substance is represented using the entity
class or one of its subtypes. The performer of a substance
administration may be another entity such as a person, device, plant,
e.g. poison ivy, animal, e.g. mosquito bite, or it may be the same
entity as the subject, as in self-administration. In the intent moods,
substance administration represents the plan to apply a given material.
This includes (but is not limited to) prescriptions in which it might
also be associated with a request for supply. In event mood, substance
administration represents a record of the actual application of a substance.
On the left hand side of the RIM picture we see the �Entity� hierarchy,
with notable specializations �Person� and �Organization. Entities are
linked together in �Roles�: a �Patient� is a specialization of �Role�
where the player is the person that is patient, and the scoper is the
organization where the person is patient. �Roles� can �participate� in
�Acts�. These participations are registered using the �Participation� class.
[1]: �HL7 Reference Information Model� http://www.hl7.org/implement/standards/rim.cfm
http://www.hl7.org/implement/standards/rim.cfm
[2]: � Influences of the Unified Service Action Model on the HL7 Reference Information Model.� https://www.ncbi.nlm.nih.gov/pmc/articles/PMC2232835/
Reference Information Model.�
https://www.ncbi.nlm.nih.gov/pmc/articles/PMC2232835/
[3]: History of the HL7 RIM http://www.ringholm.de/docs/04500_en_History_of_the_HL7_RIM.htm
http://www.ringholm.de/docs/04500_en_History_of_the_HL7_RIM.htm
[4]: �Charles Peirce� http://www.newworldencyclopedia.org/entry/Charles_Peirce
http://www.newworldencyclopedia.org/entry/Charles_Peirce
Attachments:
On 02/06/2014 10:19 PM, Craig Ringer wrote:
On 02/06/2014 12:43 PM, Craig Ringer wrote:
1. Try (again) to do row-security in the rewriter. This was previously
impossible because of the definition of row-security behaviour around
inheritance, but with the simplified inheritance model now proposed I
think it's possible.Thanks to the simplified requirements for inheritance, this turns out to
be fairly easy. There's a version rewritten to use the rewriter in the tag:rls-9.4-upd-sb-views-v6
... which was totally wrong, and I blame lack of sleep for it ever
getting pushed. I didn't understand the rewriter as well as I thought.
v7 applies row-security quals in fireRIRrules .
It handles recursion correctly now, and works fine for both target and
non-target relations. I've cleaned out most of the cruft from the
previous optimizer based approach too.
I haven't figured out how to pass the plan invalidation information (so
plans are invalidated properly when they depend on row security quals)
down into the planner yet, that's next.
COPY still just ERROR's if you try to copy to/from a rel with
row-security quals, but again, just a matter of getting it done, I have
KaiGai's patch to work from.
Regression tests fail, including a segfault in the executor. Cause as
yet unknown, but it takes a hairy view+rowsecrule combo to trigger it.
New tag:
rls-9.4-upd-sb-views-v6
The regression test expected file needs adjustment to match the new
inheritance rules, and to cover a couple of new tests I've added.
Still true.
Need to cherry-pick the docs patch back on top of the patch tree now
things are settling.
Still true.
--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 2014-02-11 09:36, Craig Ringer wrote:
On 02/06/2014 10:19 PM, Craig Ringer wrote:
On 02/06/2014 12:43 PM, Craig Ringer wrote:
1. Try (again) to do row-security in the rewriter. This was previously
impossible because of the definition of row-security behaviour around
inheritance, but with the simplified inheritance model now proposed I
think it's possible.Thanks to the simplified requirements for inheritance, this turns out to
be fairly easy. There's a version rewritten to use the rewriter in the tag:rls-9.4-upd-sb-views-v6
... which was totally wrong, and I blame lack of sleep for it ever
getting pushed. I didn't understand the rewriter as well as I thought.v7 applies row-security quals in fireRIRrules .
New tag:
rls-9.4-upd-sb-views-v6
Hi Craig,
This looks to be the same v6 version as the initial rewriter version.
https://github.com/ringerc/postgres/commits/rls-9.4-upd-sb-views-v6
regards,
Yeb
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 02/11/2014 06:05 PM, Yeb Havinga wrote:
On 2014-02-11 09:36, Craig Ringer wrote:
On 02/06/2014 10:19 PM, Craig Ringer wrote:
On 02/06/2014 12:43 PM, Craig Ringer wrote:
1. Try (again) to do row-security in the rewriter. This was previously
impossible because of the definition of row-security behaviour around
inheritance, but with the simplified inheritance model now proposed I
think it's possible.Thanks to the simplified requirements for inheritance, this turns out to
be fairly easy. There's a version rewritten to use the rewriter in
the tag:rls-9.4-upd-sb-views-v6
... which was totally wrong, and I blame lack of sleep for it ever
getting pushed. I didn't understand the rewriter as well as I thought.v7 applies row-security quals in fireRIRrules .
New tag:
rls-9.4-upd-sb-views-v6
Hi Craig,
This looks to be the same v6 version as the initial rewriter version.
https://github.com/ringerc/postgres/commits/rls-9.4-upd-sb-views-v6
Whoops, wrong paste.
rls-9.4-upd-sb-views-v7
--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 2014-02-11 12:09, Craig Ringer wrote:
On 02/11/2014 06:05 PM, Yeb Havinga wrote:
On 2014-02-11 09:36, Craig Ringer wrote:
On 02/06/2014 10:19 PM, Craig Ringer wrote:
On 02/06/2014 12:43 PM, Craig Ringer wrote:
1. Try (again) to do row-security in the rewriter. This was previously
impossible because of the definition of row-security behaviour around
inheritance, but with the simplified inheritance model now proposed I
think it's possible.Thanks to the simplified requirements for inheritance, this turns out to
be fairly easy. There's a version rewritten to use the rewriter in
the tag:rls-9.4-upd-sb-views-v6
... which was totally wrong, and I blame lack of sleep for it ever
getting pushed. I didn't understand the rewriter as well as I thought.v7 applies row-security quals in fireRIRrules .
New tag:rls-9.4-upd-sb-views-v6
Hi Craig,
This looks to be the same v6 version as the initial rewriter version.
https://github.com/ringerc/postgres/commits/rls-9.4-upd-sb-views-v6Whoops, wrong paste.
rls-9.4-upd-sb-views-v7
Hi Craig,
I compared output of psql -ef of the minirim.sql script posted earlier
in /messages/by-id/52F54927.1040102@gmail.com
between v4 and v7.
Not everything is ok.
Seq Scan on patient (cost=0.00..29589.31 rows=495 width=52)
Filter: (SubPlan 1)
SubPlan 1
@@ -555,7 +592,7 @@
-> Materialize (cost=26.39..570.62 rows=1014 width=4)
-> Subquery Scan on act (cost=26.39..565.55
rows=1014 width=4)
-> Nested Loop Semi Join
(cost=26.39..555.41 rows=1014 width=108)
- Join Filter: (((part.act = act_1.id)
AND (emp_2.pgname = ("current_user"())::text)) OR (NOT
((act_1.confidentialitycode)::text[] @> '{s}'::text[])))
+ Join Filter: (((part.act = act_1.id)
AND (emp_2.pgname = ("current_user"())::text)) OR (NOT
((act_1.effectivetime)::text[] @> '{s}'::text[])))
-> Append (cost=0.00..31.19
rows=1019 width=108)
-> Seq Scan on act act_1
(cost=0.00..1.59 rows=59 width=108)
@@ -587,12 +624,8 @@
FROM patient, person, organization
WHERE patient.player = person.id
AND patient.scoper = organization.id;
- id | vipcode | name | birthtime | name
-----+---------+----------+---------------------+--------------------------------
- 10 | | John Doe | 1963-04-01 00:00:00 | Community Health and
Hospitals
- 16 | | John Doe | 1963-04-01 00:00:00 | Community Mental
Health Clinic
-(2 rows)
-
+psql:/home/m/minirim2.sql:409: ERROR: attribute 6 has wrong type
+DETAIL: Table has type tsrange, but query expects _confidentialitycode.
@@ -629,7 +662,4 @@
SET SESSION AUTHORIZATION sigmund;
SET
SELECT * FROM test;
- id | classcode | moodcode | code | confidentialitycode | effectivetime
-----+-----------+----------+------+---------------------+---------------
-(0 rows)
-
+psql:/home/m/minirim2.sql:439: connection to server was lost
regards,
Yeb Havinga
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 02/11/2014 08:19 PM, Yeb Havinga wrote:
On 2014-02-11 12:09, Craig Ringer wrote:
rls-9.4-upd-sb-views-v7
Hi Craig,
I compared output of psql -ef of the minirim.sql script posted earlier
in /messages/by-id/52F54927.1040102@gmail.com
between v4 and v7.Not everything is ok.
+psql:/home/m/minirim2.sql:409: ERROR: attribute 6 has wrong type +DETAIL: Table has type tsrange, but query expects _confidentialitycode.
That's downright strange. I'll need to look into that one.
+psql:/home/m/minirim2.sql:439: connection to server was lost
I've seen a server crash for causes as yet unknown in the main
regression tests too.
I'd love to stick with the in-optimizer approach used in v4, which -
after all - works. The trouble is that it cannot support row-security
quals that incorporate views correctly. I would have to invoke the
rewriter from the optimizer and deal with recursion detection to make
that work, and it looks pretty ugly.
--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 02/11/2014 08:19 PM, Yeb Havinga wrote:
I compared output of psql -ef of the minirim.sql script posted earlier
in /messages/by-id/52F54927.1040102@gmail.com
between v4 and v7.Not everything is ok.
+psql:/home/m/minirim2.sql:409: ERROR: attribute 6 has wrong type +DETAIL: Table has type tsrange, but query expects _confidentialitycode.
This looks like an issue with attribute transformations made when
applying security barrier quals.
+psql:/home/m/minirim2.sql:439: connection to server was lost
That's dying with:
Program received signal SIGABRT, Aborted.
0x0000003f02c359e9 in __GI_raise (sig=sig@entry=6) at ../nptl/sysdeps/unix/sysv/linux/raise.c:56
56 return INLINE_SYSCALL (tgkill, 3, pid, selftid, sig);
(gdb) bt
#0 0x0000003f02c359e9 in __GI_raise (sig=sig@entry=6) at ../nptl/sysdeps/unix/sysv/linux/raise.c:56
#1 0x0000003f02c370f8 in __GI_abort () at abort.c:90
#2 0x0000000000758d9d in ExceptionalCondition (conditionName=conditionName@entry=0x8b7bf0 "!(!bms_is_empty(upper_varnos))",
errorType=errorType@entry=0x78e89c "FailedAssertion", fileName=fileName@entry=0x8b78d7 "subselect.c", lineNumber=lineNumber@entry=1394) at assert.c:54
#3 0x000000000061de2b in convert_EXISTS_sublink_to_join (root=<optimized out>, sublink=<optimized out>, under_not=under_not@entry=0 '\000', available_rels=0x117d038)
at subselect.c:1394
#4 0x000000000061efa9 in pull_up_sublinks_qual_recurse (root=root@entry=0x117d058, node=0x117a0f8, jtlink1=jtlink1@entry=0x7fff6d97f708,
available_rels1=available_rels1@entry=0x117d038, jtlink2=jtlink2@entry=0x0, available_rels2=available_rels2@entry=0x0) at prepjointree.c:391
#5 0x000000000061f339 in pull_up_sublinks_jointree_recurse (root=root@entry=0x117d058, jtnode=0x117a040, relids=relids@entry=0x7fff6d97f758) at prepjointree.c:208
#6 0x000000000062066f in pull_up_sublinks (root=root@entry=0x117d058) at prepjointree.c:147
#7 0x000000000061967e in subquery_planner (glob=0x10c3fb8, parse=parse@entry=0x1179af8, parent_root=parent_root@entry=0x1182fd0, hasRecursion=hasRecursion@entry=0 '\000',
`
#8 0x00000000005fc093 in set_subquery_pathlist (root=root@entry=0x1182fd0, rel=rel@entry=0x1179370, rti=rti@entry=4, rte=rte@entry=0x1183980) at allpaths.c:1209
#9 0x00000000005fc54e in set_rel_size (root=root@entry=0x1182fd0, rel=0x1179370, rti=rti@entry=4, rte=0x1183980) at allpaths.c:265
#10 0x00000000005fc62e in set_base_rel_sizes (root=root@entry=0x1182fd0) at allpaths.c:180
#11 0x00000000005fd584 in make_one_rel (root=root@entry=0x1182fd0, joinlist=joinlist@entry=0x1179560) at allpaths.c:138
#12 0x000000000061617a in query_planner (root=root@entry=0x1182fd0, tlist=tlist@entry=0x11771c8, qp_callback=qp_callback@entry=0x616fd6 <standard_qp_callback>,
qp_extra=qp_extra@entry=0x7fff6d97fa00) at planmain.c:236
#13 0x00000000006182f2 in grouping_planner (root=root@entry=0x1182fd0, tuple_fraction=tuple_fraction@entry=0) at planner.c:1280
#14 0x0000000000619a11 in subquery_planner (glob=glob@entry=0x10c3fb8, parse=parse@entry=0x10c3d30, parent_root=parent_root@entry=0x0,
hasRecursion=hasRecursion@entry=0 '\000', tuple_fraction=0, subroot=subroot@entry=0x7fff6d97fb58) at planner.c:574
#15 0x0000000000619c1f in standard_planner (parse=0x10c3d30, cursorOptions=0, boundParams=0x0) at planner.c:211
#16 0x0000000000619e35 in planner (parse=parse@entry=0x10c3d30, cursorOptions=cursorOptions@entry=0, boundParams=boundParams@entry=0x0) at planner.c:139
#17 0x000000000068c64b in pg_plan_query (querytree=0x10c3d30, cursorOptions=cursorOptions@entry=0, boundParams=boundParams@entry=0x0) at postgres.c:759
#18 0x000000000068c6d3 in pg_plan_queries (querytrees=<optimized out>, cursorOptions=cursorOptions@entry=0, boundParams=boundParams@entry=0x0) at postgres.c:818
#19 0x000000000068c932 in exec_simple_query (query_string=query_string@entry=0x10c2d78 "SELECT * FROM hl7.test;") at postgres.c:983
#20 0x000000000068e46e in PostgresMain (argc=<optimized out>, argv=argv@entry=0x10cd1f0, dbname=0x10cd058 "regress", username=<optimized out>) at postgres.c:4003
#21 0x00000000006419a7 in BackendRun (port=port@entry=0x10c7e50) at postmaster.c:4080
#22 0x00000000006432c2 in BackendStartup (port=port@entry=0x10c7e50) at postmaster.c:3769
#23 0x0000000000643526 in ServerLoop () at postmaster.c:1580
#24 0x000000000064467c in PostmasterMain (argc=argc@entry=4, argv=argv@entry=0x10a8750) at postmaster.c:1235
#25 0x00000000005d692c in main (argc=4, argv=0x10a8750) at main.c:205
(gdb)
It's crashing while pulling up the query over "emp" (hl7.employee) and
"part" (hl7.participation).
Given the simplicity of what the row-security code its self is doing,
I'm wondering if this is a case that isn't handled in updatable s.b.
views. I'll look into it.
--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 13 February 2014 04:12, Craig Ringer <craig@2ndquadrant.com> wrote:
On 02/11/2014 08:19 PM, Yeb Havinga wrote:
I compared output of psql -ef of the minirim.sql script posted earlier
in /messages/by-id/52F54927.1040102@gmail.com
between v4 and v7.Not everything is ok.
+psql:/home/m/minirim2.sql:409: ERROR: attribute 6 has wrong type +DETAIL: Table has type tsrange, but query expects _confidentialitycode.This looks like an issue with attribute transformations made when
applying security barrier quals.+psql:/home/m/minirim2.sql:439: connection to server was lost
That's dying with:
Program received signal SIGABRT, Aborted.
0x0000003f02c359e9 in __GI_raise (sig=sig@entry=6) at ../nptl/sysdeps/unix/sysv/linux/raise.c:56
56 return INLINE_SYSCALL (tgkill, 3, pid, selftid, sig);
(gdb) bt
#0 0x0000003f02c359e9 in __GI_raise (sig=sig@entry=6) at ../nptl/sysdeps/unix/sysv/linux/raise.c:56
#1 0x0000003f02c370f8 in __GI_abort () at abort.c:90
#2 0x0000000000758d9d in ExceptionalCondition (conditionName=conditionName@entry=0x8b7bf0 "!(!bms_is_empty(upper_varnos))",
errorType=errorType@entry=0x78e89c "FailedAssertion", fileName=fileName@entry=0x8b78d7 "subselect.c", lineNumber=lineNumber@entry=1394) at assert.c:54
#3 0x000000000061de2b in convert_EXISTS_sublink_to_join (root=<optimized out>, sublink=<optimized out>, under_not=under_not@entry=0 '\000', available_rels=0x117d038)
at subselect.c:1394
#4 0x000000000061efa9 in pull_up_sublinks_qual_recurse (root=root@entry=0x117d058, node=0x117a0f8, jtlink1=jtlink1@entry=0x7fff6d97f708,
available_rels1=available_rels1@entry=0x117d038, jtlink2=jtlink2@entry=0x0, available_rels2=available_rels2@entry=0x0) at prepjointree.c:391
#5 0x000000000061f339 in pull_up_sublinks_jointree_recurse (root=root@entry=0x117d058, jtnode=0x117a040, relids=relids@entry=0x7fff6d97f758) at prepjointree.c:208
#6 0x000000000062066f in pull_up_sublinks (root=root@entry=0x117d058) at prepjointree.c:147
#7 0x000000000061967e in subquery_planner (glob=0x10c3fb8, parse=parse@entry=0x1179af8, parent_root=parent_root@entry=0x1182fd0, hasRecursion=hasRecursion@entry=0 '\000',`
#8 0x00000000005fc093 in set_subquery_pathlist (root=root@entry=0x1182fd0, rel=rel@entry=0x1179370, rti=rti@entry=4, rte=rte@entry=0x1183980) at allpaths.c:1209
#9 0x00000000005fc54e in set_rel_size (root=root@entry=0x1182fd0, rel=0x1179370, rti=rti@entry=4, rte=0x1183980) at allpaths.c:265
#10 0x00000000005fc62e in set_base_rel_sizes (root=root@entry=0x1182fd0) at allpaths.c:180
#11 0x00000000005fd584 in make_one_rel (root=root@entry=0x1182fd0, joinlist=joinlist@entry=0x1179560) at allpaths.c:138
#12 0x000000000061617a in query_planner (root=root@entry=0x1182fd0, tlist=tlist@entry=0x11771c8, qp_callback=qp_callback@entry=0x616fd6 <standard_qp_callback>,
qp_extra=qp_extra@entry=0x7fff6d97fa00) at planmain.c:236
#13 0x00000000006182f2 in grouping_planner (root=root@entry=0x1182fd0, tuple_fraction=tuple_fraction@entry=0) at planner.c:1280
#14 0x0000000000619a11 in subquery_planner (glob=glob@entry=0x10c3fb8, parse=parse@entry=0x10c3d30, parent_root=parent_root@entry=0x0,
hasRecursion=hasRecursion@entry=0 '\000', tuple_fraction=0, subroot=subroot@entry=0x7fff6d97fb58) at planner.c:574
#15 0x0000000000619c1f in standard_planner (parse=0x10c3d30, cursorOptions=0, boundParams=0x0) at planner.c:211
#16 0x0000000000619e35 in planner (parse=parse@entry=0x10c3d30, cursorOptions=cursorOptions@entry=0, boundParams=boundParams@entry=0x0) at planner.c:139
#17 0x000000000068c64b in pg_plan_query (querytree=0x10c3d30, cursorOptions=cursorOptions@entry=0, boundParams=boundParams@entry=0x0) at postgres.c:759
#18 0x000000000068c6d3 in pg_plan_queries (querytrees=<optimized out>, cursorOptions=cursorOptions@entry=0, boundParams=boundParams@entry=0x0) at postgres.c:818
#19 0x000000000068c932 in exec_simple_query (query_string=query_string@entry=0x10c2d78 "SELECT * FROM hl7.test;") at postgres.c:983
#20 0x000000000068e46e in PostgresMain (argc=<optimized out>, argv=argv@entry=0x10cd1f0, dbname=0x10cd058 "regress", username=<optimized out>) at postgres.c:4003
#21 0x00000000006419a7 in BackendRun (port=port@entry=0x10c7e50) at postmaster.c:4080
#22 0x00000000006432c2 in BackendStartup (port=port@entry=0x10c7e50) at postmaster.c:3769
#23 0x0000000000643526 in ServerLoop () at postmaster.c:1580
#24 0x000000000064467c in PostmasterMain (argc=argc@entry=4, argv=argv@entry=0x10a8750) at postmaster.c:1235
#25 0x00000000005d692c in main (argc=4, argv=0x10a8750) at main.c:205
(gdb)It's crashing while pulling up the query over "emp" (hl7.employee) and
"part" (hl7.participation).Given the simplicity of what the row-security code its self is doing,
I'm wondering if this is a case that isn't handled in updatable s.b.
views. I'll look into it.
I'm not sure how much further you've got with this, but I think the
issue is that the securityQuals that you're adding don't refer to the
correct RTE. When adding securityQuals to an RTE, they are expected to
have Vars whose varno matches the rt_index of the RTE (see for example
the code in rewriteTargetView() which calls ChangeVarNodes() on
viewqual before adding the qual to securityQuals or the main query
jointree). prepend_row_security_quals() doesn't appear to have any
similar code, and it would need to be passed the rt_index to do that.
Regards,
Dean
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 02/25/2014 01:28 AM, Dean Rasheed wrote:
On 13 February 2014 04:12, Craig Ringer <craig@2ndquadrant.com> wrote:
It's crashing while pulling up the query over "emp" (hl7.employee) and
"part" (hl7.participation).Given the simplicity of what the row-security code its self is doing,
I'm wondering if this is a case that isn't handled in updatable s.b.
views. I'll look into it.I'm not sure how much further you've got with this, but I think the
issue is that the securityQuals that you're adding don't refer to the
correct RTE. When adding securityQuals to an RTE, they are expected to
have Vars whose varno matches the rt_index of the RTE (see for example
the code in rewriteTargetView() which calls ChangeVarNodes() on
viewqual before adding the qual to securityQuals or the main query
jointree). prepend_row_security_quals() doesn't appear to have any
similar code, and it would need to be passed the rt_index to do that.
Thanks for the pointer. That was indeed the issue.
I've pushed an update to the branch with the fix for varno handling.
Thanks. It's tagged rls-9.4-upd-sb-views-v8 .
I've almost run out of time to spend on row security for this
commitfest, unfortunately. I'm putting a blog together with a current
status update. Frustrating, as it's coming together now.
Open issues include:
- Passing plan inval items from rewriter into planner
- COPY support pending
- Clear syntax in DDL
Most of the rest are solved; it's actually looking pretty good.
--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 04/03/14 02:36, Craig Ringer wrote:
On 02/25/2014 01:28 AM, Dean Rasheed wrote:
On 13 February 2014 04:12, Craig Ringer <craig@2ndquadrant.com> wrote:
It's crashing while pulling up the query over "emp" (hl7.employee) and
"part" (hl7.participation).Given the simplicity of what the row-security code its self is doing,
I'm wondering if this is a case that isn't handled in updatable s.b.
views. I'll look into it.I'm not sure how much further you've got with this, but I think the
issue is that the securityQuals that you're adding don't refer to the
correct RTE. When adding securityQuals to an RTE, they are expected to
have Vars whose varno matches the rt_index of the RTE (see for example
the code in rewriteTargetView() which calls ChangeVarNodes() on
viewqual before adding the qual to securityQuals or the main query
jointree). prepend_row_security_quals() doesn't appear to have any
similar code, and it would need to be passed the rt_index to do that.Thanks for the pointer. That was indeed the issue.
I've pushed an update to the branch with the fix for varno handling.
Thanks. It's tagged rls-9.4-upd-sb-views-v8 .I've almost run out of time to spend on row security for this
commitfest, unfortunately. I'm putting a blog together with a current
status update. Frustrating, as it's coming together now.Open issues include:
- Passing plan inval items from rewriter into planner
- COPY support pending
- Clear syntax in DDLMost of the rest are solved; it's actually looking pretty good.
Hi Craig,
I've tested the results from the minirim.sql that was posted earlier,
and the v8 gives the same results as v4 :-)
Thanks for all the work!
Yeb
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 04/03/14 02:36, Craig Ringer wrote:
On 02/25/2014 01:28 AM, Dean Rasheed wrote:
On 13 February 2014 04:12, Craig Ringer <craig@2ndquadrant.com> wrote:
It's crashing while pulling up the query over "emp" (hl7.employee) and
"part" (hl7.participation).Given the simplicity of what the row-security code its self is doing,
I'm wondering if this is a case that isn't handled in updatable s.b.
views. I'll look into it.I'm not sure how much further you've got with this, but I think the
issue is that the securityQuals that you're adding don't refer to the
correct RTE. When adding securityQuals to an RTE, they are expected to
have Vars whose varno matches the rt_index of the RTE (see for example
the code in rewriteTargetView() which calls ChangeVarNodes() on
viewqual before adding the qual to securityQuals or the main query
jointree). prepend_row_security_quals() doesn't appear to have any
similar code, and it would need to be passed the rt_index to do that.Thanks for the pointer. That was indeed the issue.
I've pushed an update to the branch with the fix for varno handling.
Thanks. It's tagged rls-9.4-upd-sb-views-v8 .I've almost run out of time to spend on row security for this
commitfest, unfortunately. I'm putting a blog together with a current
status update. Frustrating, as it's coming together now.Open issues include:
- Passing plan inval items from rewriter into planner
- COPY support pending
- Clear syntax in DDLMost of the rest are solved; it's actually looking pretty good.
Hi Craig,
I've tested the results from the minirim.sql that was posted earlier,
and the v8 gives the same results as v4 :-)
Thanks for all the work!
Yeb
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 03/04/2014 09:41 PM, Yeb Havinga wrote:
On 04/03/14 02:36, Craig Ringer wrote:
I've pushed an update to the branch with the fix for varno handling.
Thanks. It's tagged rls-9.4-upd-sb-views-v8 .I've almost run out of time to spend on row security for this
commitfest, unfortunately. I'm putting a blog together with a current
status update. Frustrating, as it's coming together now.Open issues include:
- Passing plan inval items from rewriter into planner
- COPY support pending
- Clear syntax in DDLMost of the rest are solved; it's actually looking pretty good.
Hi Craig,
I've tested the results from the minirim.sql that was posted earlier,
and the v8 gives the same results as v4 :-)
Good to hear.
The main known issue remaining is plan invalidation. Row security needs
to create PlanInvalItems during _rewrite_ and also needs to set a
PlannerGlobal field for the user ID the plan depends on. If it fails to
do this then the superuser will sometimes run queries and have
row-security applied, non-superusers might skip row security when it
should be applied. This seems to be the cause of foreign key check
issues I've observed, too.
That's not trivial, because right now the rewriter only returns a Query
node. It doesn't have anywhere to stash information that's global across
the whole query, and adding fields to Query for the purpose doesn't seem
ideal, since it's also used for subqueries, and in the planner. Changing
the rewriter to return a RewriteResult struct that contains the Query
and some other global info would be very intrusive, though, as it'd
affect all the plan cache and invalidation machinery and the various
rewriter/planner call sites.
I've also got some concerns about the user visible API; I'm not sure it
makes sense to set row security policy for row reads per-command in
PostgreSQL, since we have the RETURNING clause. Read-side policy should
just be "FOR READ". Initially I think we should be using triggers to
enforce write side policy, which should raise errors if you try to
insert/update/delete the wrong thing. Could move to something working a
bit more like WITH CHECK OPTION later.
--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 2014-03-05 04:02, Craig Ringer wrote:
On 03/04/2014 09:41 PM, Yeb Havinga wrote:
On 04/03/14 02:36, Craig Ringer wrote:
I've pushed an update to the branch with the fix for varno handling.
Thanks. It's tagged rls-9.4-upd-sb-views-v8 .I've almost run out of time to spend on row security for this
commitfest, unfortunately. I'm putting a blog together with a current
status update. Frustrating, as it's coming together now.Open issues include:
- Passing plan inval items from rewriter into planner
- COPY support pending
- Clear syntax in DDLMost of the rest are solved; it's actually looking pretty good.
Hi Craig,
I've tested the results from the minirim.sql that was posted earlier,
and the v8 gives the same results as v4 :-)Good to hear.
The main known issue remaining is plan invalidation. Row security needs
to create PlanInvalItems during _rewrite_ and also needs to set a
PlannerGlobal field for the user ID the plan depends on. If it fails to
do this then the superuser will sometimes run queries and have
row-security applied, non-superusers might skip row security when it
should be applied. This seems to be the cause of foreign key check
issues I've observed, too.That's not trivial, because right now the rewriter only returns a Query
node. It doesn't have anywhere to stash information that's global across
the whole query, and adding fields to Query for the purpose doesn't seem
ideal, since it's also used for subqueries, and in the planner.
I looked up the Query structure and steps of e.g. exec_simple_query(),
but ISTM that Query would be the place to store a used id. After all it
is meta data about the query. Duplication of this information in the
presence of subqueries seems less ugly to me than trying to evade
duplication by wrapping a structure around a query list.
Maybe a naive thought, but shouldn't all plans that include a table with
an RLS clause be invalidated when the session role switches, regardless
of which users from and to?
I've also got some concerns about the user visible API; I'm not sure it
makes sense to set row security policy for row reads per-command in
PostgreSQL, since we have the RETURNING clause. Read-side policy should
just be "FOR READ".
Hmm but FOR READ would mean new keywords, and SELECT is also a concept
known to users. I didn't find the api problematic to understand, on the
contrary. It might be an idea to add the SELECT RLS clause for DML
queries that contain a RETURNING clause.
regards,
Yeb
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 03/05/2014 05:25 PM, Yeb Havinga wrote:
Maybe a naive thought, but shouldn't all plans that include a table with
an RLS clause be invalidated when the session role switches, regardless
of which users from and to?
Only if the plan is actually accessed when under a different user ID.
Consider SECURITY DEFINER functions; you don't want to flush all cached
plans just because you ran a SECURITY DEFINER function that doesn't even
share any statements with the outer transaction.
Anyway, the same issue remains: how to pass the information "this plan
is user-id specific" from rewriter to planner.
I've also got some concerns about the user visible API; I'm not sure it
makes sense to set row security policy for row reads per-command in
PostgreSQL, since we have the RETURNING clause. Read-side policy should
just be "FOR READ".Hmm but FOR READ would mean new keywords, and SELECT is also a concept
known to users. I didn't find the api problematic to understand, on the
contrary.
Would you expect that FOR SELECT also affects rows you can see to
UPDATE, INSERT, or DELETE?
Because that's what it would have to mean, really. Otherwise, you could
just use `UPDATE thetable SET id = id RETURNING *` (or whatever) to read
the rows out if you had UPDATE rights. Or do the same with DELETE.
With RETURNING, it doesn't make much sense for different statements to
have different read access. Can you think of a case where it'd be
reasonable to deny SELECT, but allow someone to see the same rows with
`UPDATE ... RETURNING` ?
It might be an idea to add the SELECT RLS clause for DML
queries that contain a RETURNING clause.
That way lies madness: A DML statement that affects *a different set of
rows* depending on whether or not it has a RETURNING clause.
--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 05/03/14 15:44, Craig Ringer wrote:
On 03/05/2014 05:25 PM, Yeb Havinga wrote:
Maybe a naive thought, but shouldn't all plans that include a table with
an RLS clause be invalidated when the session role switches, regardless
of which users from and to?Only if the plan is actually accessed when under a different user ID.
Consider SECURITY DEFINER functions; you don't want to flush all cached
plans just because you ran a SECURITY DEFINER function that doesn't even
share any statements with the outer transaction.
Hmm good point.
I've also got some concerns about the user visible API; I'm not sure it
makes sense to set row security policy for row reads per-command in
PostgreSQL, since we have the RETURNING clause. Read-side policy should
just be "FOR READ".Hmm but FOR READ would mean new keywords, and SELECT is also a concept
known to users. I didn't find the api problematic to understand, on the
contrary.Would you expect that FOR SELECT also affects rows you can see to
UPDATE, INSERT, or DELETE?
Yes.
Because that's what it would have to mean, really. Otherwise, you could
just use `UPDATE thetable SET id = id RETURNING *` (or whatever) to read
the rows out if you had UPDATE rights. Or do the same with DELETE.With RETURNING, it doesn't make much sense for different statements to
have different read access. Can you think of a case where it'd be
reasonable to deny SELECT, but allow someone to see the same rows with
`UPDATE ... RETURNING` ?It might be an idea to add the SELECT RLS clause for DML
queries that contain a RETURNING clause.That way lies madness: A DML statement that affects *a different set of
rows* depending on whether or not it has a RETURNING clause.
If you state it like that, it sounds like a POLA violation. But the
complete story is: "A user is allowed to UPDATE a set of rows from a
table that is not a subsect of the set of rows he can SELECT from the
table, iow he can UPDATE rows he is not allowed to SELECT. This can lead
to unexpected results: When the user issues an UPDATE of the table
without a returning clause, all rows the user may UPDATE are affected.
When the user issues an UPDATE of the table with a returning clause, all
rows the user may UPDATE and SELECT are affected."
So the madness comes from the fact that it is allowed to define RLS that
allow to modify rows you cannot select. Either prevent these conditions
(i.e. proof that all DML RLS qual implies the SELECT qual, otherwise
give an error on DML with a RETURNING clause), or allow it without
violating the RLS rules but accept that a DML with RETURNING is
different from a DML only.
regards,
Yeb
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 03/06/2014 04:56 AM, Yeb Havinga wrote:
It might be an idea to add the SELECT RLS clause for DML
queries that contain a RETURNING clause.That way lies madness: A DML statement that affects *a different set of
rows* depending on whether or not it has a RETURNING clause.If you state it like that, it sounds like a POLA violation. But the
complete story is: "A user is allowed to UPDATE a set of rows from a
table that is not a subsect of the set of rows he can SELECT from the
table, iow he can UPDATE rows he is not allowed to SELECT. This can lead
to unexpected results: When the user issues an UPDATE of the table
without a returning clause, all rows the user may UPDATE are affected.
When the user issues an UPDATE of the table with a returning clause, all
rows the user may UPDATE and SELECT are affected."
Well, I think that's bizarre behaviour.
It doesn't make sense for RETURNING to affect the behaviour of the
command it's applied to. It never has before, and it's defined to return
the set of rows affected by the command. It shouldn't be changing that
set, it isn't a predicate.
So the madness comes from the fact that it is allowed to define RLS that
allow to modify rows you cannot select. Either prevent these conditions
(i.e. proof that all DML RLS qual implies the SELECT qual, otherwise
give an error on DML with a RETURNING clause)
Equivalence proofs in predicates are WAY outside what's going to be
reasonable to tackle in a feature like this. Especially since the row
security expression may be "my_c_function(the_row)", and all the detail
of behaviour is hidden behind some C function.
We need to treat row security expressions as pretty much opaque.
or allow it without
violating the RLS rules but accept that a DML with RETURNING is
different from a DML only.
I don't think that's acceptable. Too many tools automatically add a
RETURNING clause, or use one in generated SQL. Notably, PgJDBC will
append a RETURNING clause to your query so it can return generated keys.
With regular permissions, we require that the user has SELECT rights if
they use a RETURNING clause. That works because it's a simple
permissions check. With row security, we'd be affecting a different set
of rows instead, and doing so silently. That's just ugly. (It also
creates execution inefficiencies where the SELECT and UPDATE/DELETE
predicates are the same).
Additionally, in PostgreSQL if you can supply a predicate for a row, you
can leak the row via a RAISE NOTICE or other tricks. So even without
RETURNING, allowing a user to update/delete a row permits them to
potentially see the row. (This is an issue with our current permissions
too; if you DELETE with a leaky predicate, you can see the rows you
deleted even without SELECT rights on a table).
That, IMO, is two good reasons not to differentiate between command
types for the purpose of which rows they can see in a scan.
We already have a mechanism for allowing users to do things that they
can't normally do under controlled and restricted circumstances:
SECURITY DEFINER functions.
I don't think we need to introduce bizarre and surprising behaviour in
DML when we have a viable mechanism for people who need to do this.
Is there a compelling use case for this? Where it really makes sense to
let users update/delete rows they cannot see via row security? We
support it in the table based permissions model, but it's possible to do
it with much saner semantics there. And with row security, it'll be
possible with security definer functions. I intend to add a "row
security exempt" flag for functions down the track, too.
--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 06/03/14 02:56, Craig Ringer wrote:
On 03/06/2014 04:56 AM, Yeb Havinga wrote:
If you state it like that, it sounds like a POLA violation. But the
complete story is: "A user is allowed to UPDATE a set of rows from a
table that is not a subsect of the set of rows he can SELECT from the
table, iow he can UPDATE rows he is not allowed to SELECT.
Is there a compelling use case for this? Where it really makes sense to
let users update/delete rows they cannot see via row security? We
support it in the table based permissions model, but it's possible to do
it with much saner semantics there. And with row security, it'll be
possible with security definer functions. I intend to add a "row
security exempt" flag for functions down the track, too.
Use case: https://en.wikipedia.org/wiki/Bell-La_Padula_model - being
able to write up and read down access levels.
So for instance in healthcare, a data enterer may enter from hand
written notes sensitive data (like subject has aids) in the electronic
health record, without having general read access of the level of
sensitivity of aids diagnosis. I think what is important in use cases
like this, is that at data entry time, the actual data is still at the
desk, so having data returned for inserts in the running transaction
might not be problematic. As most EHR's today are additive in nature,
future additions about the aids conditions would be inserts as well, no
updates required. For updates my best guess would be that allowing the
command to run with rls permissions different from the select is not
required.
regards,
Yeb
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 03/05/2014 11:02 AM, Craig Ringer wrote:
The main known issue remaining is plan invalidation.
I've pushed a version with a plan invalidation implementation. It's tagged:
rls-9.4-upd-sb-views-v9
in
git@github.com:ringerc/postgres.git
The invalidation implementation does not yet handle foreign key checks;
that will require additional changes. I'll push an update to the
rls-9.4-upd-sb-views and post an update later, at which time I'll rebase
the changes back into the history.
--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 03/07/2014 09:33 PM, Craig Ringer wrote:
On 03/05/2014 11:02 AM, Craig Ringer wrote:
The main known issue remaining is plan invalidation.
I've pushed a version with a plan invalidation implementation. It's tagged:
rls-9.4-upd-sb-views-v9
in
git@github.com:ringerc/postgres.git
The invalidation implementation does not yet handle foreign key checks;
that will require additional changes. I'll push an update to the
rls-9.4-upd-sb-views and post an update later, at which time I'll rebase
the changes back into the history.
Well, that was easy. Done.
rls-9.4-upd-sb-views-v11
and rebased the rls-9.4-upd-sb-views branch to incorporate the changes.
The regression tests have further failures, but some are due to changes
in the inheritance semantics. I'm going through them now.
--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 03/07/2014 10:07 PM, Craig Ringer wrote:
On 03/07/2014 09:33 PM, Craig Ringer wrote:
On 03/05/2014 11:02 AM, Craig Ringer wrote:
The main known issue remaining is plan invalidation.
I've pushed a version with a plan invalidation implementation. It's tagged:
rls-9.4-upd-sb-views-v9
in
git@github.com:ringerc/postgres.git
The invalidation implementation does not yet handle foreign key checks;
that will require additional changes. I'll push an update to the
rls-9.4-upd-sb-views and post an update later, at which time I'll rebase
the changes back into the history.Well, that was easy. Done.
rls-9.4-upd-sb-views-v11
and rebased the rls-9.4-upd-sb-views branch to incorporate the changes.
The regression tests have further failures, but some are due to changes
in the inheritance semantics. I'm going through them now.
Need a quick opinion.
KaiGai's original code produced a plan like this for an inheritance set:
EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
! QUERY PLAN
! -------------------------------------------
LockRows
! -> Append
! -> Subquery Scan on t1
! Filter: f_leak(t1.b)
! -> Seq Scan on t1 t1_1
! Filter: ((a % 2) = 0)
! -> Subquery Scan on t2
! Filter: f_leak(t2.b)
! -> Seq Scan on t2 t2_1
! Filter: ((a % 2) = 1)
! -> Seq Scan on t3
! Filter: f_leak(b)
(12 rows)
The new code, using updatable s.b. views, instead produces:
EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
! QUERY PLAN
! -------------------------------------------------------
LockRows
! -> Subquery Scan on t1
! Filter: f_leak(t1.b)
! -> LockRows
! -> Result
! -> Append
! -> Seq Scan on t1 t1_1
! Filter: ((a % 2) = 0)
! -> Seq Scan on t2
! Filter: ((a % 2) = 0)
! -> Seq Scan on t3
! Filter: ((a % 2) = 0)
(12 rows)
The different quals are expected, because of the change to the
definition of inheritance handling in row security.
What I'm concerned about is the locking. It looks to me like we're
causing the user to lock rows that they may not intend to lock, by
applying a LockRows step *before* the user supplied qual. (I'm going to
test that tomorrow, it's sleep time in Australia).
This seems to be related to RowMark handling in updatable security
barrier views. I need to check whether it happens with updates to
security barrier views, as well as with row security.
Dean, any thoughts?
--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Craig Ringer <craig@2ndquadrant.com> writes:
What I'm concerned about is the locking. It looks to me like we're
causing the user to lock rows that they may not intend to lock, by
applying a LockRows step *before* the user supplied qual. (I'm going to
test that tomorrow, it's sleep time in Australia).
The fact that there are two LockRows nodes seems outright broken.
The one at the top of the plan is correctly placed, but how did the
other one get in there?
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 03/08/2014 01:56 AM, Tom Lane wrote:
Craig Ringer <craig@2ndquadrant.com> writes:
What I'm concerned about is the locking. It looks to me like we're
causing the user to lock rows that they may not intend to lock, by
applying a LockRows step *before* the user supplied qual. (I'm going to
test that tomorrow, it's sleep time in Australia).The fact that there are two LockRows nodes seems outright broken.
The one at the top of the plan is correctly placed, but how did the
other one get in there?
I initially thought it was the updatable security barrier views code
pushing the RowMark down into the generated subquery. But if I remove
the pushdown code the inner LockRows node still seems to get emitted.
In fact, it's not a new issue. In vanilla 9.3.1:
regress=> select version();
version
--------------------------------------------------------------------------------------------------------------
PostgreSQL 9.3.1 on x86_64-unknown-linux-gnu, compiled by gcc (GCC)
4.8.1 20130603 (Red Hat 4.8.1-1), 64-bit
(1 row)
regress=> CREATE TABLE t1(x integer, y integer);
CREATE TABLE
regress=> INSERT INTO t1(x,y) VALUES (1,1), (2,2), (3,3), (4,4);
INSERT 0 4
regress=> CREATE VIEW v1 WITH (security_barrier) AS SELECT x, y FROM t1
WHERE x % 2 = 0;
CREATE VIEW
regress=> CREATE OR REPLACE FUNCTION user_qual() RETURNS boolean AS $$
BEGIN RETURN TRUE; END;
$$ LANGUAGE plpgsql;
CREATE FUNCTION
regress=> EXPLAIN SELECT * FROM v1 WHERE user_qual() FOR UPDATE;
QUERY PLAN
-----------------------------------------------------------------------
LockRows (cost=0.00..45.11 rows=4 width=40)
-> Subquery Scan on v1 (cost=0.00..45.07 rows=4 width=40)
Filter: user_qual()
-> LockRows (cost=0.00..42.21 rows=11 width=14)
-> Seq Scan on t1 (cost=0.00..42.10 rows=11 width=14)
Filter: ((x % 2) = 0)
(6 rows)
so it looks like security barrier views are locking rows they should not be.
I can confirm that on 9.3.1 with:
CREATE OR REPLACE FUNCTION row_is(integer, integer) RETURNS boolean as
$$ begin return (select $1 = $2); end; $$ language plpgsql;
then in two sessions:
SELECT * FROM v1 WHERE row_is(x, 2) FOR UPDATE;
and
SELECT * FROM v1 WHERE row_is(x, 4) FOR UPDATE;
These should not block each other, but do.
So there's a pre-existing bug here.
--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers