doc/src/sgml/rules.sgml | 95 +++++++++++++++++
src/test/regress/expected/create_view.out | 34 ++++++-
src/test/regress/expected/select_views_1.out | 141 ++++++++++++++++++++++++++
src/test/regress/sql/create_view.sql | 17 +++
src/test/regress/sql/select_views.sql | 65 ++++++++++++
5 files changed, 351 insertions(+), 1 deletions(-)
diff --git a/doc/src/sgml/rules.sgml b/doc/src/sgml/rules.sgml
index 1b06519..4ccb076 100644
--- a/doc/src/sgml/rules.sgml
+++ b/doc/src/sgml/rules.sgml
@@ -1856,6 +1856,101 @@ SELECT * FROM phone_number WHERE tricky(person, phone);
+ In addition, another scenarios is known to leak invisible tuples, even if
+ these should be filtered out by view.
+
+CREATE VIEW your_credit AS
+ SELECT a.rolname, c.number, c.expire
+ FROM pg_authid a JOIN credit_cards c ON a.oid = c.id
+ WHERE a.rolname = getpgusername();
+
+ This view may also look secure, because all the references to the
+ your_credit are restricted to the tuples which
+ satisfies a.rolname = getpgusername().
+ Unfortunately, this assumption is incorrect.
+
+
+ Note that this view contains a join loop. If a user provides
+ WHERE clause a function that references only
+ one-side of the join loop, the query planner distributes this
+ qualifier inside of the loop to minimize the number of tuples to
+ be joined.
+
+ It is an example to break the row-level security implemented with
+ your_credit view:
+
+postgres=> SELECT * FROM your_credit WHERE tricky(number, expire);
+NOTICE: 1111-2222-3333-4444 => Jan-01
+NOTICE: 5555-6666-7777-8888 => Feb-02
+NOTICE: 1234-5678-9012-3456 => Mar-03
+ rolname | number | expire
+---------+---------------------+--------
+ alice | 5555-6666-7777-8888 | Feb-02
+(1 row)
+
+
+
+ The output of EXPLAIN shows us obvious reason
+ of this unpreferable result.
+
+postgres=> EXPLAIN SELECT * FROM your_credit WHERE tricky(number, expire);
+ QUERY PLAN
+------------------------------------------------------------------------
+ Hash Join (cost=1.03..20.38 rows=1 width=128)
+ Hash Cond: (c.id = a.oid)
+ -> Seq Scan on credit_cards c (cost=0.00..18.30 rows=277 width=68)
+ Filter: tricky(number, expire)
+ -> Hash (cost=1.01..1.01 rows=1 width=68)
+ -> Seq Scan on pg_authid a (cost=0.00..1.01 rows=1 width=68)
+ Filter: (rolname = getpgusername())
+(7 rows)
+
+ The supplied tricky only references
+ number and expire columns
+ of credit_cards relation, however, the qualifier
+ to restrict invisible tuples performs at another side of the join
+ loop.
+ In the result, the supplied tricky was launched
+ during the executor scans credit_cards table, then
+ it raised messages including given arguments that contains information
+ to be invisible.
+
+
+ PostgreSQL provides a countermeasure for
+ these scenarios. CREATE SECURITY VIEW allows to
+ define viewa with a hint that informs the query planner this view is
+ intended to filter out invisible tuples; in other word, it shall be
+ used to row-level security purpose, instead of a bit performance
+ trade-off.
+
+
+ If the query planner found a view with this hint, it never launches
+ qualifiers provided at outside of the view earlier than qualifiers
+ used inside of the view, independent from its cost estimation,
+ even if these qualifiers are chained to same scan plan togetger.
+ In addition, the query planner never push down the qualifiers
+ across the view with this hint, even if it references one-side of
+ the join loop only, except for LEAKPROOF functions.
+
+
+ No need to say, it has performance trade-off, because it restain
+ a part of query optimization to be done in regular cases.
+ Especially, it was expected that performance trade-off is unacceptable
+ in the case when index-scan was degraded to sequential-scan due to
+ prevention of qualifier distribution to optimal scan plan.
+ So, we also provided a way to provide the query optimizer a hint that
+ shows the function is obviously leakproof, so no need to prevent
+ push down the qualifiers into join-loops.
+ It is LEAKPROOF option of
+ CREATE FUNCTION; that allows only superusers to
+ define functions with this flag.
+
+
+ These options allows to find out the most suitable combination of
+ security and performance. DBA should set up views of row-level
+ security using these options, with understanding to the background.
+
+
Similar considerations apply to update rules. In the examples of
the previous section, the owner of the tables in the example
database could grant the privileges SELECT>,
diff --git a/src/test/regress/expected/create_view.out b/src/test/regress/expected/create_view.out
index f9490a3..9e92173 100644
--- a/src/test/regress/expected/create_view.out
+++ b/src/test/regress/expected/create_view.out
@@ -239,6 +239,36 @@ And relnamespace IN (SELECT OID FROM pg_namespace WHERE nspname LIKE 'pg_temp%')
1
(1 row)
+-- the view shall be marked as 'security view'
+CREATE SECURITY VIEW mysecview1
+ AS SELECT * FROM tbl1 WHERE a > 0;
+SELECT relname, relkind, relissecbarrier FROM pg_class
+ WHERE oid = 'mysecview1'::regclass;
+ relname | relkind | relissecbarrier
+------------+---------+-----------------
+ mysecview1 | v | t
+(1 row)
+
+-- 'security view' flag shall be preserved
+CREATE OR REPLACE VIEW mysecview1
+ AS SELECT * FROM tbl1 WHERE a < 0;
+SELECT relname, relkind, relissecbarrier FROM pg_class
+ WHERE oid = 'mysecview1'::regclass;
+ relname | relkind | relissecbarrier
+------------+---------+-----------------
+ mysecview1 | v | t
+(1 row)
+
+-- the default of 'security view' is false
+CREATE OR REPLACE VIEW mysecview2
+ AS SELECT * FROM tbl1 WHERE a = 0;
+SELECT relname, relkind, relissecbarrier FROM pg_class
+ WHERE oid = 'mysecview2'::regclass;
+ relname | relkind | relissecbarrier
+------------+---------+-----------------
+ mysecview2 | v | f
+(1 row)
+
DROP SCHEMA temp_view_test CASCADE;
NOTICE: drop cascades to 22 other objects
DETAIL: drop cascades to table temp_view_test.base_table
@@ -264,7 +294,7 @@ drop cascades to view temp_view_test.v8
drop cascades to sequence temp_view_test.seq1
drop cascades to view temp_view_test.v9
DROP SCHEMA testviewschm2 CASCADE;
-NOTICE: drop cascades to 16 other objects
+NOTICE: drop cascades to 18 other objects
DETAIL: drop cascades to table t1
drop cascades to view temporal1
drop cascades to view temporal2
@@ -281,4 +311,6 @@ drop cascades to table tbl3
drop cascades to table tbl4
drop cascades to view mytempview
drop cascades to view pubview
+drop cascades to view mysecview1
+drop cascades to view mysecview2
SET search_path to public;
diff --git a/src/test/regress/expected/select_views_1.out b/src/test/regress/expected/select_views_1.out
index 9a972cf..1d1f9e9 100644
--- a/src/test/regress/expected/select_views_1.out
+++ b/src/test/regress/expected/select_views_1.out
@@ -1247,3 +1247,144 @@ SELECT * FROM toyemp WHERE name = 'sharon';
sharon | 25 | (15,12) | 12000
(1 row)
+--
+-- Test for Leaky view scenario
+--
+CREATE USER alice;
+CREATE FUNCTION f_leak (text)
+ RETURNS bool LANGUAGE 'plpgsql'
+ COST 0.0000001
+ AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END';
+CREATE FUNCTION f_leakproof (text)
+ RETURNS bool LANGUAGE 'plpgsql' LEAKPROOF
+ COST 0.0000001
+ AS 'BEGIN RAISE NOTICE ''f_leakproof => %'', $1; RETURN true; END';
+CREATE TABLE customer (
+ cid int primary key,
+ name text,
+ addr text,
+ passwd text
+);
+NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "customer_pkey" for table "customer"
+CREATE TABLE credit_cards (
+ cid int references customer(cid),
+ cardno text,
+ expired text
+);
+INSERT INTO customer
+ VALUES (101, 'alice', 'Japan', 'passwd123'),
+ (102, 'bob', 'USA', 'beafsteak'),
+ (103, 'eve', 'Germany', 'hamburger');
+INSERT INTO credit_cards
+ VALUES (101, '1111-2222-3333-4444', 'Aug-2012'),
+ (102, '5555-6666-7777-8888', 'Nov-2016'),
+ (103, '9801-2345-6789-0123', 'Jan-2018');
+CREATE VIEW your_property_normal AS
+ SELECT * FROM customer WHERE name = getpgusername();
+CREATE SECURITY VIEW your_property_secure AS
+ SELECT * FROM customer WHERE name = getpgusername();
+CREATE VIEW your_credit_card_normal AS
+ SELECT l.name, r.* FROM customer l NATURAL JOIN credit_cards r
+ WHERE name = getpgusername();
+CREATE SECURITY VIEW your_credit_card_secure AS
+ SELECT l.name, r.* FROM customer l NATURAL JOIN credit_cards r
+ WHERE name = getpgusername();
+GRANT SELECT ON your_property_normal TO public;
+GRANT SELECT ON your_property_secure TO public;
+GRANT SELECT ON your_credit_card_normal TO public;
+GRANT SELECT ON your_credit_card_secure TO public;
+---
+--- Run leaky view scenarios
+---
+SET SESSION AUTHORIZATION alice;
+SELECT * FROM your_property_normal WHERE f_leak(passwd);
+NOTICE: f_leak => passwd123
+NOTICE: f_leak => beafsteak
+NOTICE: f_leak => hamburger
+ cid | name | addr | passwd
+-----+-------+-------+-----------
+ 101 | alice | Japan | passwd123
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM your_property_normal WHERE f_leak(passwd);
+ QUERY PLAN
+-----------------------------------------------------------------
+ Seq Scan on customer
+ Filter: (f_leak(passwd) AND (name = (getpgusername())::text))
+(2 rows)
+
+SELECT * FROM your_property_secure WHERE f_leak(passwd);
+NOTICE: f_leak => passwd123
+ cid | name | addr | passwd
+-----+-------+-------+-----------
+ 101 | alice | Japan | passwd123
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM your_property_secure WHERE f_leak(passwd);
+ QUERY PLAN
+-----------------------------------------------------------------
+ Seq Scan on customer
+ Filter: ((name = (getpgusername())::text) AND f_leak(passwd))
+(2 rows)
+
+SELECT * FROM your_credit_card_normal WHERE f_leak(cardno);
+NOTICE: f_leak => 1111-2222-3333-4444
+NOTICE: f_leak => 5555-6666-7777-8888
+NOTICE: f_leak => 9801-2345-6789-0123
+ name | cid | cardno | expired
+-------+-----+---------------------+----------
+ alice | 101 | 1111-2222-3333-4444 | Aug-2012
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM your_credit_card_normal WHERE f_leak(cardno);
+ QUERY PLAN
+--------------------------------------------------------
+ Hash Join
+ Hash Cond: (r.cid = l.cid)
+ -> Seq Scan on credit_cards r
+ Filter: f_leak(cardno)
+ -> Hash
+ -> Seq Scan on customer l
+ Filter: (name = (getpgusername())::text)
+(7 rows)
+
+SELECT * FROM your_credit_card_secure WHERE f_leak(cardno);
+NOTICE: f_leak => 1111-2222-3333-4444
+ name | cid | cardno | expired
+-------+-----+---------------------+----------
+ alice | 101 | 1111-2222-3333-4444 | Aug-2012
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM your_credit_card_secure WHERE f_leak(cardno);
+ QUERY PLAN
+--------------------------------------------------------
+ Hash Join
+ Hash Cond: (r.cid = l.cid)
+ Join Filter: f_leak(r.cardno)
+ -> Seq Scan on credit_cards r
+ -> Hash
+ -> Seq Scan on customer l
+ Filter: (name = (getpgusername())::text)
+(7 rows)
+
+SELECT * FROM your_credit_card_secure WHERE f_leakproof(cardno);
+NOTICE: f_leakproof => 1111-2222-3333-4444
+NOTICE: f_leakproof => 5555-6666-7777-8888
+NOTICE: f_leakproof => 9801-2345-6789-0123
+ name | cid | cardno | expired
+-------+-----+---------------------+----------
+ alice | 101 | 1111-2222-3333-4444 | Aug-2012
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM your_credit_card_secure WHERE f_leakproof(cardno);
+ QUERY PLAN
+--------------------------------------------------------
+ Hash Join
+ Hash Cond: (r.cid = l.cid)
+ -> Seq Scan on credit_cards r
+ Filter: f_leakproof(cardno)
+ -> Hash
+ -> Seq Scan on customer l
+ Filter: (name = (getpgusername())::text)
+(7 rows)
+
diff --git a/src/test/regress/sql/create_view.sql b/src/test/regress/sql/create_view.sql
index 86cfc51..43cf622 100644
--- a/src/test/regress/sql/create_view.sql
+++ b/src/test/regress/sql/create_view.sql
@@ -191,6 +191,23 @@ AND NOT EXISTS (SELECT g FROM tbl4 LEFT JOIN tmptbl ON tbl4.h = tmptbl.j);
SELECT count(*) FROM pg_class where relname LIKE 'mytempview'
And relnamespace IN (SELECT OID FROM pg_namespace WHERE nspname LIKE 'pg_temp%');
+-- the view shall be marked as 'security view'
+CREATE SECURITY VIEW mysecview1
+ AS SELECT * FROM tbl1 WHERE a > 0;
+SELECT relname, relkind, relissecbarrier FROM pg_class
+ WHERE oid = 'mysecview1'::regclass;
+
+-- 'security view' flag shall be preserved
+CREATE OR REPLACE VIEW mysecview1
+ AS SELECT * FROM tbl1 WHERE a < 0;
+SELECT relname, relkind, relissecbarrier FROM pg_class
+ WHERE oid = 'mysecview1'::regclass;
+-- the default of 'security view' is false
+CREATE OR REPLACE VIEW mysecview2
+ AS SELECT * FROM tbl1 WHERE a = 0;
+SELECT relname, relkind, relissecbarrier FROM pg_class
+ WHERE oid = 'mysecview2'::regclass;
+
DROP SCHEMA temp_view_test CASCADE;
DROP SCHEMA testviewschm2 CASCADE;
diff --git a/src/test/regress/sql/select_views.sql b/src/test/regress/sql/select_views.sql
index 14f1be8..d1ac965 100644
--- a/src/test/regress/sql/select_views.sql
+++ b/src/test/regress/sql/select_views.sql
@@ -8,3 +8,68 @@ SELECT * FROM street;
SELECT name, #thepath FROM iexit ORDER BY 1, 2;
SELECT * FROM toyemp WHERE name = 'sharon';
+
+--
+-- Test for Leaky view scenario
+--
+CREATE USER alice;
+CREATE FUNCTION f_leak (text)
+ RETURNS bool LANGUAGE 'plpgsql'
+ COST 0.0000001
+ AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END';
+CREATE FUNCTION f_leakproof (text)
+ RETURNS bool LANGUAGE 'plpgsql' LEAKPROOF
+ COST 0.0000001
+ AS 'BEGIN RAISE NOTICE ''f_leakproof => %'', $1; RETURN true; END';
+CREATE TABLE customer (
+ cid int primary key,
+ name text,
+ addr text,
+ passwd text
+);
+CREATE TABLE credit_cards (
+ cid int references customer(cid),
+ cardno text,
+ expired text
+);
+INSERT INTO customer
+ VALUES (101, 'alice', 'Japan', 'passwd123'),
+ (102, 'bob', 'USA', 'beafsteak'),
+ (103, 'eve', 'Germany', 'hamburger');
+INSERT INTO credit_cards
+ VALUES (101, '1111-2222-3333-4444', 'Aug-2012'),
+ (102, '5555-6666-7777-8888', 'Nov-2016'),
+ (103, '9801-2345-6789-0123', 'Jan-2018');
+
+CREATE VIEW your_property_normal AS
+ SELECT * FROM customer WHERE name = getpgusername();
+CREATE SECURITY VIEW your_property_secure AS
+ SELECT * FROM customer WHERE name = getpgusername();
+CREATE VIEW your_credit_card_normal AS
+ SELECT l.name, r.* FROM customer l NATURAL JOIN credit_cards r
+ WHERE name = getpgusername();
+CREATE SECURITY VIEW your_credit_card_secure AS
+ SELECT l.name, r.* FROM customer l NATURAL JOIN credit_cards r
+ WHERE name = getpgusername();
+
+GRANT SELECT ON your_property_normal TO public;
+GRANT SELECT ON your_property_secure TO public;
+GRANT SELECT ON your_credit_card_normal TO public;
+GRANT SELECT ON your_credit_card_secure TO public;
+---
+--- Run leaky view scenarios
+---
+SET SESSION AUTHORIZATION alice;
+
+SELECT * FROM your_property_normal WHERE f_leak(passwd);
+EXPLAIN (COSTS OFF) SELECT * FROM your_property_normal WHERE f_leak(passwd);
+
+SELECT * FROM your_property_secure WHERE f_leak(passwd);
+EXPLAIN (COSTS OFF) SELECT * FROM your_property_secure WHERE f_leak(passwd);
+
+SELECT * FROM your_credit_card_normal WHERE f_leak(cardno);
+EXPLAIN (COSTS OFF) SELECT * FROM your_credit_card_normal WHERE f_leak(cardno);
+SELECT * FROM your_credit_card_secure WHERE f_leak(cardno);
+EXPLAIN (COSTS OFF) SELECT * FROM your_credit_card_secure WHERE f_leak(cardno);
+SELECT * FROM your_credit_card_secure WHERE f_leakproof(cardno);
+EXPLAIN (COSTS OFF) SELECT * FROM your_credit_card_secure WHERE f_leakproof(cardno);