pg.ddx.io pgsql-hackers@postgresql.org mailing list archive
help / color / mirror / Atom feed From: Greg Smith <greg@2ndQuadrant.com>
To: Karol Trzcionka <karlikt@gmail.com>
Cc: Mike Blackwell <mike.blackwell@rrd.com>
Cc: Kohei KaiGai <kaigai@kaigai.gr.jp>
Cc: pgsql-hackers@postgresql.org
Subject: Re: [v9.4] row level security
Date: Thu, 18 Jul 2013 22:47:29 -0400
Message-ID: <51E8A8C1.2050700@2ndQuadrant.com> (raw )
In-Reply-To: <51E880CF.3000902@gmail.com >
References: <CADyhKSUq+BHPAEdrNQXy_bFSzz2SG29vzJmDUh4p20iWje8QSg@mail.gmail.com >
<CADyhKSVNMMvmPCQkpaVb96T4F3Eu+uREnak4_5_5RLOuSeJLQg@mail.gmail.com >
<CADyhKSXFzQBXQoNwiKdN9xxoVw7Zqz9gmF64Sry57QKduSFwSQ@mail.gmail.com >
<CANPAkgtgZbOE5-iU17Gy+_YWODe5UBsfZg6+rk2v=bNQRo0wgA@mail.gmail.com >
<CADyhKSVoCHMnnGX0Z2b8dgfCUoE1XGdtbAz2HXE4+Q3ajgoxrQ@mail.gmail.com >
<CANPAkgvcL+w2scZqEcXwX5-TMe9EEORLcHDesJeTn8yoZsc0=g@mail.gmail.com >
<51E880CF.3000902@gmail.com >
List-Unsubscribe: <mailto:majordomo@postgresql.org?body=unsub%20pgsql-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
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;
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Attachments:
[text/plain] pgsql-v9.4-row-level-security.v3b.patch (169.0K, ../51E8A8C1.2050700@2ndQuadrant.com/2-pgsql-v9.4-row-level-security.v3b.patch)
download | inline diff:
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;
view thread (165+ messages) latest in thread
Message-ID: <51E8A8C1.2050700@2ndQuadrant.com>
Permalink: ../51E8A8C1.2050700@2ndQuadrant.com/
Also on: postgresql.org/message-id/51E8A8C1.2050700@2ndQuadrant.com
copy link · copy postgr.es · copy git am
reply Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Reply to all the recipients using the --to and --cc options:
reply via email
To: pgsql-hackers@postgresql.org
Cc: greg@2ndQuadrant.com, karlikt@gmail.com, mike.blackwell@rrd.com, kaigai@kaigai.gr.jp
Subject: Re: [v9.4] row level security
In-Reply-To: <51E8A8C1.2050700@2ndQuadrant.com>
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
This inbox is served by DDX for PostgreSQL; see mirroring instructions
for how to clone and mirror all data and code used for this inbox