diff --git a/doc/src/sgml/ref/alter_view.sgml b/doc/src/sgml/ref/alter_view.sgml index c383905..3340856 100644 --- a/doc/src/sgml/ref/alter_view.sgml +++ b/doc/src/sgml/ref/alter_view.sgml @@ -26,6 +26,8 @@ ALTER VIEW name ALTER [ COLUMN ] name OWNER TO new_owner ALTER VIEW name RENAME TO new_name ALTER VIEW name SET SCHEMA new_schema +ALTER VIEW name SET ( parameter [= value] [, ... ] ) +ALTER VIEW name RESET ( parameter [, ... ] ) @@ -102,6 +104,24 @@ ALTER VIEW name SET SCHEMA + + + parameter + + + Name of the view option to be set. + + + + + + value + + + The new value for the view option. + + + diff --git a/doc/src/sgml/ref/create_view.sgml b/doc/src/sgml/ref/create_view.sgml index 417f8c3..9dba6a5 100644 --- a/doc/src/sgml/ref/create_view.sgml +++ b/doc/src/sgml/ref/create_view.sgml @@ -22,6 +22,7 @@ PostgreSQL documentation CREATE [ OR REPLACE ] [ TEMP | TEMPORARY ] VIEW name [ ( column_name [, ...] ) ] + [ WITH ( parameter [= value] [, ... ] ) ] AS query @@ -99,6 +100,29 @@ CREATE [ OR REPLACE ] [ TEMP | TEMPORARY ] VIEW n + WITH (parameter [= value]) + + + This clause allows to specify optional parameters for a view. + + + If security_barrier=TRUE is specified, this view + shall performs as security barrier that prevent unexpected information + leaks. It is a recommendable configuration when the view is defined + to apply row-level security, in spite of performance trade-off. + + + It is a commonly-used technique that using views to filter out + tuple to be invisible to particular users, however, please note + that here is a known-problem that allows malicious users to + reference invisible tuples using a function with side-effect + because of interaction with query optimization. + See for more detailed scenario. + + + + + query diff --git a/doc/src/sgml/rules.sgml b/doc/src/sgml/rules.sgml index 1b06519..421d777 100644 --- a/doc/src/sgml/rules.sgml +++ b/doc/src/sgml/rules.sgml @@ -1854,7 +1854,102 @@ SELECT * FROM phone_number WHERE tricky(person, phone); similar attacks. (For example, most casting functions include their input values in the error messages they produce.) + + In addition, another scenarios is also known to leak invisible tuples + unlike fair expectations. + +CREATE VIEW my_credit_cards AS + SELECT a.rolname, c.number, c.expire + FROM pg_authid a JOIN credit_cards c ON a.oid = c.id + WHERE a.rolname = current_user; + + This view may also look secure, because all the references to the + my_credit_cards are restricted to the tuples that + satisfies a.rolname = current_user, so it shall + prevent to reference the card numbers of other persons. + + + Note that this view contains a join loop. If user provides + WHERE clause a function that references only + one-side of the join loop, the query planner distributes this + qualifier inside of the join loop to minimize the number of tuples + to be joined. + It is an example to break the row-level security implemented with + my_credit_cards view: + +postgres=> SELECT * FROM my_credit_cards 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 the unpredicated result. + +postgres=> EXPLAIN SELECT * FROM my_credit_cards 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 = "current_user"()) +(7 rows) + + The supplied tricky references + only number and expire columns + of credit_cards relation, however, the qualifier + to restrict invisible tuples performs at another side on the join loop. + In the result, the query planner distributes the supplied + tricky on the sequential scan plan on the + credit_cards table, then the query executor launches + this query towards all the tuples and it allows to raise messages + that contains information to be invisible. + + + PostgreSQL provides a countermeasure of + these scenarios. If a particular sub-query originated a view with + security_barrier option, the planner does not + push down qualifiers originated from outside of the view, although + it has a trade-off between performance and stable security. + + The security_barrier option shall modify the query + plan of the above example of my_credit_cards as + follows. You may see the tricky is launched + after all the invisible tuples being filtered out, if this option + was added in the view definition. + +postgres=> SELECT * FROM my_credit_cards WHERE tricky(number, expire); +NOTICE: 1111-2222-3333-4444 => Jan-01 + rolname | number | expire +---------+---------------------+-------- + alice | 1111-2222-3333-4444 | Jan-01 +(1 row) + +postgres=> EXPLAIN SELECT * FROM my_credit_cards WHERE tricky(number, expire); + QUERY PLAN +------------------------------------------------------------------------------ + Subquery Scan on my_credit_cards (cost=1.03..22.52 rows=1 width=128) + Filter: tricky(my_credit_cards.number, my_credit_cards.expire) + -> Hash Join (cost=1.03..22.48 rows=4 width=128) + Hash Cond: (c.id = a.oid) + -> Seq Scan on credit_cards c (cost=0.00..18.30 rows=830 width=68) + -> 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 = "current_user"()) +(8 rows) + + So, we recommend to append this option when views are defined to + provide row-level security for unprivileged users. + Similar considerations apply to update rules. In the examples of the previous section, the owner of the tables in the example diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c index 240e178..78a06b8 100644 --- a/src/backend/access/common/reloptions.c +++ b/src/backend/access/common/reloptions.c @@ -66,6 +66,14 @@ static relopt_bool boolRelOpts[] = }, true }, + { + { + "security_barrier", + "Prevent pushing down qualifiers come from outside of the view", + RELOPT_KIND_VIEW + }, + false + }, /* list terminator */ {{NULL}} }; @@ -772,6 +780,7 @@ extractRelOptions(HeapTuple tuple, TupleDesc tupdesc, Oid amoptions) { case RELKIND_RELATION: case RELKIND_TOASTVALUE: + case RELKIND_VIEW: case RELKIND_UNCATALOGED: options = heap_reloptions(classForm->relkind, datum, false); break; @@ -1130,7 +1139,9 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind) {"autovacuum_vacuum_scale_factor", RELOPT_TYPE_REAL, offsetof(StdRdOptions, autovacuum) +offsetof(AutoVacOpts, vacuum_scale_factor)}, {"autovacuum_analyze_scale_factor", RELOPT_TYPE_REAL, - offsetof(StdRdOptions, autovacuum) +offsetof(AutoVacOpts, analyze_scale_factor)} + offsetof(StdRdOptions, autovacuum) +offsetof(AutoVacOpts, analyze_scale_factor)}, + {"security_barrier", RELOPT_TYPE_BOOL, + offsetof(StdRdOptions, security_barrier)}, }; options = parseRelOptions(reloptions, validate, kind, &numoptions); @@ -1172,6 +1183,8 @@ heap_reloptions(char relkind, Datum reloptions, bool validate) return (bytea *) rdopts; case RELKIND_RELATION: return default_reloptions(reloptions, validate, RELOPT_KIND_HEAP); + case RELKIND_VIEW: + return default_reloptions(reloptions, validate, RELOPT_KIND_VIEW); default: /* other relkinds are not supported */ return NULL; diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index c4622c0..b2e8487 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -2985,7 +2985,7 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd, break; case AT_SetRelOptions: /* SET (...) */ case AT_ResetRelOptions: /* RESET (...) */ - ATSimplePermissions(rel, ATT_TABLE | ATT_INDEX); + ATSimplePermissions(rel, ATT_TABLE | ATT_INDEX | ATT_VIEW); /* This command never recurses */ /* No command-specific prep needed */ pass = AT_PASS_MISC; @@ -8100,6 +8100,7 @@ ATExecSetRelOptions(Relation rel, List *defList, bool isReset, LOCKMODE lockmode { case RELKIND_RELATION: case RELKIND_TOASTVALUE: + case RELKIND_VIEW: (void) heap_reloptions(rel->rd_rel->relkind, newOptions, true); break; case RELKIND_INDEX: diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c index b238199..2fd8cd0 100644 --- a/src/backend/commands/view.c +++ b/src/backend/commands/view.c @@ -32,6 +32,7 @@ #include "utils/builtins.h" #include "utils/lsyscache.h" #include "utils/rel.h" +#include "utils/syscache.h" static void checkViewTupleDesc(TupleDesc newdesc, TupleDesc olddesc); @@ -98,7 +99,7 @@ isViewOnTempTable_walker(Node *node, void *context) */ static Oid DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace, - Oid namespaceId) + Oid namespaceId, List *options) { Oid viewOid; CreateStmt *createStmt = makeNode(CreateStmt); @@ -166,6 +167,8 @@ DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace, { Relation rel; TupleDesc descriptor; + List *atcmds = NIL; + AlterTableCmd *atcmd; /* * Yes. Get exclusive lock on the existing view ... @@ -210,14 +213,11 @@ DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace, */ if (list_length(attrList) > rel->rd_att->natts) { - List *atcmds = NIL; ListCell *c; int skip = rel->rd_att->natts; foreach(c, attrList) { - AlterTableCmd *atcmd; - if (skip > 0) { skip--; @@ -228,10 +228,25 @@ DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace, atcmd->def = (Node *) lfirst(c); atcmds = lappend(atcmds, atcmd); } - AlterTableInternal(viewOid, atcmds, true); } /* + * If view options were explicitly provided by users, + * existing values of the view being replaced are overrided, + * elseehere, rest of values shall be preserved, if exists. + */ + if (options != NIL) + { + atcmd = makeNode(AlterTableCmd); + atcmd->subtype = AT_SetRelOptions; + atcmd->def = (Node *)options; + atcmds = lappend(atcmds, atcmd); + } + + if (atcmds != NIL) + AlterTableInternal(viewOid, atcmds, true); + + /* * Seems okay, so return the OID of the pre-existing view. */ relation_close(rel, NoLock); /* keep the lock! */ @@ -255,6 +270,9 @@ DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace, createStmt->tablespacename = NULL; createStmt->if_not_exists = false; + if (options != NIL) + createStmt->options = list_concat(createStmt->options, options); + /* * finally create the relation (this will error out if there's an * existing view, so we don't need more code to complain if "replace" @@ -513,7 +531,7 @@ DefineView(ViewStmt *stmt, const char *queryString) * aborted. */ viewOid = DefineVirtualRelation(view, viewParse->targetList, - stmt->replace, namespaceId); + stmt->replace, namespaceId, stmt->options); /* * The relation we have just created is not visible to any other commands diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c index 815b996..269a289 100644 --- a/src/backend/optimizer/path/allpaths.c +++ b/src/backend/optimizer/path/allpaths.c @@ -718,6 +718,7 @@ set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel, double tuple_fraction; PlannerInfo *subroot; List *pathkeys; + bool security_barrier; /* * Must copy the Query so that planning doesn't mess up the RTE contents @@ -731,6 +732,13 @@ set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel, palloc0((list_length(subquery->targetList) + 1) * sizeof(bool)); /* + * We need special treatment on pushing-down of qualifiers with + * leakable functions across sub-queries originated from a view + * with "security_barrier" option. + */ + security_barrier = subquery_was_security_barrier(rte); + + /* * If there are any restriction clauses that have been attached to the * subquery relation, consider pushing them down to become WHERE or HAVING * quals of the subquery itself. This transformation is useful because it @@ -763,6 +771,7 @@ set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel, Node *clause = (Node *) rinfo->clause; if (!rinfo->pseudoconstant && + !security_barrier && qual_is_pushdown_safe(subquery, rti, clause, differentTypes)) { /* Push it down */ diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c index aeaae8c..f4ef799 100644 --- a/src/backend/optimizer/prep/prepjointree.c +++ b/src/backend/optimizer/prep/prepjointree.c @@ -543,6 +543,7 @@ pull_up_subqueries(PlannerInfo *root, Node *jtnode, */ if (rte->rtekind == RTE_SUBQUERY && is_simple_subquery(rte->subquery) && + !subquery_was_security_barrier(rte) && (containing_appendrel == NULL || is_safe_append_member(rte->subquery))) return pull_up_simple_subquery(root, jtnode, rte, diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index baa90fa..06586db 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -19,7 +19,9 @@ #include "postgres.h" +#include "access/reloptions.h" #include "catalog/pg_aggregate.h" +#include "catalog/pg_class.h" #include "catalog/pg_language.h" #include "catalog/pg_operator.h" #include "catalog/pg_proc.h" @@ -44,6 +46,7 @@ #include "utils/datum.h" #include "utils/lsyscache.h" #include "utils/memutils.h" +#include "utils/rel.h" #include "utils/syscache.h" #include "utils/typcache.h" @@ -1128,6 +1131,53 @@ contain_nonstrict_functions_walker(Node *node, void *context) context); } +/* + * subquery_was_security_barrier + * + * If the supplied sub-query was a view with "security_barrier" flag, + * this routine returns true. Elsewhere, false shall be returned. + * The planner should not make these sub-queries flatten, even if it + * satisfies is_simple_subquery to prevent unexpected pushing-down of + * qualifiers. + */ +bool +subquery_was_security_barrier(RangeTblEntry *rte) +{ + HeapTuple tuple; + Datum datum; + bool isnull; + bool result = false; + + Assert(rte->rtekind == RTE_SUBQUERY); + + /* + * If the supplied sub-query was a view, rte->relid should track + * its Oid. Elsewhere, it was just a sub-query from the begining. + */ + if (!OidIsValid(rte->relid)) + return false; + + tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(rte->relid)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for relation %u", rte->relid); + + datum = SysCacheGetAttr(RELOID, tuple, Anum_pg_class_reloptions, &isnull); + if (!isnull) + { + char relkind = ((Form_pg_class) GETSTRUCT(tuple))->relkind; + StdRdOptions *rdopts + = (StdRdOptions *)heap_reloptions(relkind, datum, false); + + if (rdopts) + { + result = rdopts->security_barrier; + pfree(rdopts); + } + } + ReleaseSysCache(tuple); + + return result; +} /* * find_nonnullable_rels diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index f0a4b0e..b91d2e4 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -7288,26 +7288,28 @@ transaction_mode_list_or_empty: * *****************************************************************************/ -ViewStmt: CREATE OptTemp VIEW qualified_name opt_column_list +ViewStmt: CREATE OptTemp VIEW qualified_name opt_column_list opt_reloptions AS SelectStmt opt_check_option { ViewStmt *n = makeNode(ViewStmt); n->view = $4; n->view->relpersistence = $2; n->aliases = $5; - n->query = $7; + n->query = $8; n->replace = false; + n->options = $6; $$ = (Node *) n; } - | CREATE OR REPLACE OptTemp VIEW qualified_name opt_column_list + | CREATE OR REPLACE OptTemp VIEW qualified_name opt_column_list opt_reloptions AS SelectStmt opt_check_option { ViewStmt *n = makeNode(ViewStmt); n->view = $6; n->view->relpersistence = $4; n->aliases = $7; - n->query = $9; + n->query = $10; n->replace = true; + n->options = $8; $$ = (Node *) n; } ; diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c index 3b31108..876c560 100644 --- a/src/backend/rewrite/rewriteHandler.c +++ b/src/backend/rewrite/rewriteHandler.c @@ -1380,7 +1380,7 @@ ApplyRetrieveRule(Query *parsetree, rte = rt_fetch(rt_index, parsetree->rtable); rte->rtekind = RTE_SUBQUERY; - rte->relid = InvalidOid; + rte->relid = RelationGetRelid(relation); rte->subquery = rule_action; rte->inh = false; /* must not be set for a subquery */ diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index c17b52c..f9402c1 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -12086,8 +12086,10 @@ dumpTableSchema(Archive *fout, TableInfo *tbinfo) if (binary_upgrade) binary_upgrade_set_pg_class_oids(q, tbinfo->dobj.catId.oid, false); - appendPQExpBuffer(q, "CREATE VIEW %s AS\n %s\n", - fmtId(tbinfo->dobj.name), viewdef); + appendPQExpBuffer(q, "CREATE VIEW %s", fmtId(tbinfo->dobj.name)); + if (tbinfo->reloptions && strlen(tbinfo->reloptions) > 0) + appendPQExpBuffer(q, " WITH (%s)", tbinfo->reloptions); + appendPQExpBuffer(q, " AS\n %s\n", viewdef); appendPQExpBuffer(labelq, "VIEW %s", fmtId(tbinfo->dobj.name)); diff --git a/src/include/access/reloptions.h b/src/include/access/reloptions.h index 14f5034..57595b0 100644 --- a/src/include/access/reloptions.h +++ b/src/include/access/reloptions.h @@ -42,8 +42,9 @@ typedef enum relopt_kind RELOPT_KIND_GIST = (1 << 5), RELOPT_KIND_ATTRIBUTE = (1 << 6), RELOPT_KIND_TABLESPACE = (1 << 7), + RELOPT_KIND_VIEW = (1 << 8), /* if you add a new kind, make sure you update "last_default" too */ - RELOPT_KIND_LAST_DEFAULT = RELOPT_KIND_TABLESPACE, + RELOPT_KIND_LAST_DEFAULT = RELOPT_KIND_VIEW, /* some compilers treat enums as signed ints, so we can't use 1 << 31 */ RELOPT_KIND_MAX = (1 << 30) } relopt_kind; diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 9998e2f..23bdccd 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -699,7 +699,8 @@ typedef struct RangeTblEntry /* * Fields valid for a plain relation RTE (else zero): */ - Oid relid; /* OID of the relation */ + Oid relid; /* OID of the relation or view if the sub- + * query originated from a particular view. */ char relkind; /* relation kind (see pg_class.relkind) */ /* @@ -2345,6 +2346,7 @@ typedef struct ViewStmt List *aliases; /* target column names */ Node *query; /* the SELECT query */ bool replace; /* replace an existing view? */ + List *options; /* options from WITH clause */ } ViewStmt; /* ---------------------- diff --git a/src/include/optimizer/clauses.h b/src/include/optimizer/clauses.h index 4cef7fa..28f1713 100644 --- a/src/include/optimizer/clauses.h +++ b/src/include/optimizer/clauses.h @@ -61,6 +61,7 @@ extern bool contain_subplans(Node *clause); extern bool contain_mutable_functions(Node *clause); extern bool contain_volatile_functions(Node *clause); extern bool contain_nonstrict_functions(Node *clause); +extern bool subquery_was_security_barrier(RangeTblEntry *rte); extern Relids find_nonnullable_rels(Node *clause); extern List *find_nonnullable_vars(Node *clause); extern List *find_forced_null_vars(Node *clause); diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h index 173dc16..c2961da 100644 --- a/src/include/utils/rel.h +++ b/src/include/utils/rel.h @@ -194,6 +194,7 @@ typedef struct StdRdOptions int32 vl_len_; /* varlena header (do not touch directly!) */ int fillfactor; /* page fill factor in percent (0..100) */ AutoVacOpts autovacuum; /* autovacuum-related options */ + bool security_barrier; /* performs as security-barrier view */ } StdRdOptions; #define HEAP_MIN_FILLFACTOR 10 @@ -222,6 +223,14 @@ typedef struct StdRdOptions (BLCKSZ * (100 - RelationGetFillFactor(relation, defaultff)) / 100) /* + * RelationIsSecurityView + * Returns whether the relation is security view, or not + */ +#define RelationIsSecurityView(relation) \ + ((relation)->rd_options ? \ + ((StdRdOptions *) (relation)->rd_options)->security_barrier : false) + +/* * RelationIsValid * True iff relation descriptor is valid. */ diff --git a/src/test/regress/expected/create_view.out b/src/test/regress/expected/create_view.out index f9490a3..432642a 100644 --- a/src/test/regress/expected/create_view.out +++ b/src/test/regress/expected/create_view.out @@ -239,6 +239,55 @@ And relnamespace IN (SELECT OID FROM pg_namespace WHERE nspname LIKE 'pg_temp%') 1 (1 row) +-- +-- CREATE VIEW and WITH(...) clause +-- +CREATE VIEW mysecview1 + AS SELECT * FROM tbl1 WHERE a = 0; +CREATE VIEW mysecview2 WITH (security_barrier=true) + AS SELECT * FROM tbl1 WHERE a > 0; +CREATE VIEW mysecview3 WITH (security_barrier=false) + AS SELECT * FROM tbl1 WHERE a < 0; +CREATE VIEW mysecview4 WITH (security_barrier) + AS SELECT * FROM tbl1 WHERE a <> 0; +CREATE VIEW mysecview5 WITH (security_barrier=100) -- Error + AS SELECT * FROM tbl1 WHERE a > 100; +ERROR: invalid value for boolean option "security_barrier": 100 +CREATE VIEW mysecview6 WITH (invalid_option) -- Error + AS SELECT * FROM tbl1 WHERE a < 100; +ERROR: unrecognized parameter "invalid_option" +SELECT relname, relkind, reloptions FROM pg_class + WHERE oid in ('mysecview1'::regclass, 'mysecview2'::regclass, + 'mysecview3'::regclass, 'mysecview4'::regclass); + relname | relkind | reloptions +------------+---------+-------------------------- + mysecview1 | v | + mysecview2 | v | {security_barrier=true} + mysecview3 | v | {security_barrier=false} + mysecview4 | v | {security_barrier=true} +(4 rows) + +-- reloptions should be preserved +CREATE OR REPLACE VIEW mysecview1 + AS SELECT * FROM tbl1 WHERE a = 256; +CREATE OR REPLACE VIEW mysecview2 + AS SELECT * FROM tbl1 WHERE a > 256; +-- reloptions should be turned on/off +CREATE OR REPLACE VIEW mysecview3 WITH (security_barrier=true) + AS SELECT * FROM tbl1 WHERE a < 256; +CREATE OR REPLACE VIEW mysecview4 WITH (security_barrier=false) + AS SELECT * FROM tbl1 WHERE a <> 256; +SELECT relname, relkind, reloptions FROM pg_class + WHERE oid in ('mysecview1'::regclass, 'mysecview2'::regclass, + 'mysecview3'::regclass, 'mysecview4'::regclass); + relname | relkind | reloptions +------------+---------+-------------------------- + mysecview1 | v | + mysecview2 | v | {security_barrier=true} + mysecview3 | v | {security_barrier=true} + mysecview4 | v | {security_barrier=false} +(4 rows) + 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 +313,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 20 other objects DETAIL: drop cascades to table t1 drop cascades to view temporal1 drop cascades to view temporal2 @@ -281,4 +330,8 @@ 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 +drop cascades to view mysecview3 +drop cascades to view mysecview4 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..20c407f 100644 --- a/src/test/regress/expected/select_views_1.out +++ b/src/test/regress/expected/select_views_1.out @@ -1247,3 +1247,216 @@ 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 TABLE customer ( + cid int primary key, + name text not null, + tel text, + passwd text +); +NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "customer_pkey" for table "customer" +CREATE TABLE credit_card ( + cid int references customer(cid), + cnum text, + climit int +); +CREATE TABLE credit_usage ( + cid int references customer(cid), + ymd date, + usage int +); +INSERT INTO customer + VALUES (101, 'alice', '+81-12-3456-7890', 'passwd123'), + (102, 'bob', '+01-234-567-8901', 'beafsteak'), + (103, 'eve', '+49-8765-43210', 'hamburger'); +INSERT INTO credit_card + VALUES (101, '1111-2222-3333-4444', 4000), + (102, '5555-6666-7777-8888', 3000), + (103, '9801-2345-6789-0123', 2000); +INSERT INTO credit_usage + VALUES (101, '2011-09-15', 120), + (101, '2011-10-05', 90), + (101, '2011-10-18', 110), + (101, '2011-10-21', 200), + (101, '2011-11-10', 80), + (102, '2011-09-22', 300), + (102, '2011-10-12', 120), + (102, '2011-10-28', 200), + (103, '2011-10-15', 480); +CREATE VIEW my_property_normal AS + SELECT * FROM customer WHERE name = current_user; +CREATE VIEW my_property_secure WITH (security_barrier) AS + SELECT * FROM customer WHERE name = current_user; +CREATE VIEW my_credit_card_normal AS + SELECT * FROM customer l NATURAL JOIN credit_card r + WHERE l.name = current_user; +CREATE VIEW my_credit_card_secure WITH (security_barrier) AS + SELECT * FROM customer l NATURAL JOIN credit_card r + WHERE l.name = current_user; +CREATE VIEW my_credit_card_usage_normal AS + SELECT * FROM my_credit_card_secure l NATURAL JOIN credit_usage r; +CREATE VIEW my_credit_card_usage_secure WITH (security_barrier) AS + SELECT * FROM my_credit_card_secure l NATURAL JOIN credit_usage r; +GRANT SELECT ON my_property_normal TO public; +GRANT SELECT ON my_property_secure TO public; +GRANT SELECT ON my_credit_card_normal TO public; +GRANT SELECT ON my_credit_card_secure TO public; +GRANT SELECT ON my_credit_card_usage_normal TO public; +GRANT SELECT ON my_credit_card_usage_secure TO public; +-- +-- Run leaky view scenarios +-- +SET SESSION AUTHORIZATION alice; +-- +-- scenario: if a qualifier with tiny-cost is given, it shall be launched +-- prior to the security policy of the view. +-- +SELECT * FROM my_property_normal WHERE f_leak(passwd); +NOTICE: f_leak => passwd123 +NOTICE: f_leak => beafsteak +NOTICE: f_leak => hamburger + cid | name | tel | passwd +-----+-------+------------------+----------- + 101 | alice | +81-12-3456-7890 | passwd123 +(1 row) + +EXPLAIN (COSTS OFF) SELECT * FROM my_property_normal WHERE f_leak(passwd); + QUERY PLAN +------------------------------------------------------------------ + Seq Scan on customer + Filter: (f_leak(passwd) AND (name = ("current_user"())::text)) +(2 rows) + +SELECT * FROM my_property_secure WHERE f_leak(passwd); +NOTICE: f_leak => passwd123 + cid | name | tel | passwd +-----+-------+------------------+----------- + 101 | alice | +81-12-3456-7890 | passwd123 +(1 row) + +EXPLAIN (COSTS OFF) SELECT * FROM my_property_secure WHERE f_leak(passwd); + QUERY PLAN +--------------------------------------------------- + Subquery Scan on my_property_secure + Filter: f_leak(my_property_secure.passwd) + -> Seq Scan on customer + Filter: (name = ("current_user"())::text) +(4 rows) + +-- +-- scenario: if a qualifier references only one-side of a particular join- +-- tree, it shall be distributed to the most deep scan plan as +-- possible as we can. +-- +SELECT * FROM my_credit_card_normal WHERE f_leak(cnum); +NOTICE: f_leak => 1111-2222-3333-4444 +NOTICE: f_leak => 5555-6666-7777-8888 +NOTICE: f_leak => 9801-2345-6789-0123 + cid | name | tel | passwd | cnum | climit +-----+-------+------------------+-----------+---------------------+-------- + 101 | alice | +81-12-3456-7890 | passwd123 | 1111-2222-3333-4444 | 4000 +(1 row) + +EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_normal WHERE f_leak(cnum); + QUERY PLAN +--------------------------------------------------------- + Hash Join + Hash Cond: (r.cid = l.cid) + -> Seq Scan on credit_card r + Filter: f_leak(cnum) + -> Hash + -> Seq Scan on customer l + Filter: (name = ("current_user"())::text) +(7 rows) + +SELECT * FROM my_credit_card_secure WHERE f_leak(cnum); +NOTICE: f_leak => 1111-2222-3333-4444 + cid | name | tel | passwd | cnum | climit +-----+-------+------------------+-----------+---------------------+-------- + 101 | alice | +81-12-3456-7890 | passwd123 | 1111-2222-3333-4444 | 4000 +(1 row) + +EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_secure WHERE f_leak(cnum); + QUERY PLAN +--------------------------------------------------------------- + Subquery Scan on my_credit_card_secure + Filter: f_leak(my_credit_card_secure.cnum) + -> Hash Join + Hash Cond: (r.cid = l.cid) + -> Seq Scan on credit_card r + -> Hash + -> Seq Scan on customer l + Filter: (name = ("current_user"())::text) +(8 rows) + +-- +-- scenario: an external qualifier can be pushed-down by in-front-of the +-- views with "security_barrier" attribute +-- +SELECT * FROM my_credit_card_usage_normal + WHERE f_leak(cnum) AND ymd >= '2011-10-01' AND ymd < '2011-11-01'; +NOTICE: f_leak => 1111-2222-3333-4444 + cid | name | tel | passwd | cnum | climit | ymd | usage +-----+-------+------------------+-----------+---------------------+--------+------------+------- + 101 | alice | +81-12-3456-7890 | passwd123 | 1111-2222-3333-4444 | 4000 | 10-05-2011 | 90 + 101 | alice | +81-12-3456-7890 | passwd123 | 1111-2222-3333-4444 | 4000 | 10-18-2011 | 110 + 101 | alice | +81-12-3456-7890 | passwd123 | 1111-2222-3333-4444 | 4000 | 10-21-2011 | 200 +(3 rows) + +EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_usage_normal + WHERE f_leak(cnum) AND ymd >= '2011-10-01' AND ymd < '2011-11-01'; + QUERY PLAN +------------------------------------------------------------------------------ + Nested Loop + Join Filter: (l.cid = r.cid) + -> Seq Scan on credit_usage r + Filter: ((ymd >= '10-01-2011'::date) AND (ymd < '11-01-2011'::date)) + -> Materialize + -> Subquery Scan on l + Filter: f_leak(l.cnum) + -> Hash Join + Hash Cond: (r.cid = l.cid) + -> Seq Scan on credit_card r + -> Hash + -> Seq Scan on customer l + Filter: (name = ("current_user"())::text) +(13 rows) + +SELECT * FROM my_credit_card_usage_secure + WHERE f_leak(cnum) AND ymd >= '2011-10-01' AND ymd < '2011-11-01'; +NOTICE: f_leak => 1111-2222-3333-4444 +NOTICE: f_leak => 1111-2222-3333-4444 +NOTICE: f_leak => 1111-2222-3333-4444 +NOTICE: f_leak => 1111-2222-3333-4444 +NOTICE: f_leak => 1111-2222-3333-4444 + cid | name | tel | passwd | cnum | climit | ymd | usage +-----+-------+------------------+-----------+---------------------+--------+------------+------- + 101 | alice | +81-12-3456-7890 | passwd123 | 1111-2222-3333-4444 | 4000 | 10-05-2011 | 90 + 101 | alice | +81-12-3456-7890 | passwd123 | 1111-2222-3333-4444 | 4000 | 10-18-2011 | 110 + 101 | alice | +81-12-3456-7890 | passwd123 | 1111-2222-3333-4444 | 4000 | 10-21-2011 | 200 +(3 rows) + +EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_usage_secure + WHERE f_leak(cnum) AND ymd >= '2011-10-01' AND ymd < '2011-11-01'; + QUERY PLAN +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Subquery Scan on my_credit_card_usage_secure + Filter: (f_leak(my_credit_card_usage_secure.cnum) AND (my_credit_card_usage_secure.ymd >= '10-01-2011'::date) AND (my_credit_card_usage_secure.ymd < '11-01-2011'::date)) + -> Hash Join + Hash Cond: (r.cid = l.cid) + -> Seq Scan on credit_usage r + -> Hash + -> Hash Join + Hash Cond: (r.cid = l.cid) + -> Seq Scan on credit_card r + -> Hash + -> Seq Scan on customer l + Filter: (name = ("current_user"())::text) +(12 rows) + diff --git a/src/test/regress/sql/create_view.sql b/src/test/regress/sql/create_view.sql index 86cfc51..657e676 100644 --- a/src/test/regress/sql/create_view.sql +++ b/src/test/regress/sql/create_view.sql @@ -191,6 +191,38 @@ 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%'); +-- +-- CREATE VIEW and WITH(...) clause +-- +CREATE VIEW mysecview1 + AS SELECT * FROM tbl1 WHERE a = 0; +CREATE VIEW mysecview2 WITH (security_barrier=true) + AS SELECT * FROM tbl1 WHERE a > 0; +CREATE VIEW mysecview3 WITH (security_barrier=false) + AS SELECT * FROM tbl1 WHERE a < 0; +CREATE VIEW mysecview4 WITH (security_barrier) + AS SELECT * FROM tbl1 WHERE a <> 0; +CREATE VIEW mysecview5 WITH (security_barrier=100) -- Error + AS SELECT * FROM tbl1 WHERE a > 100; +CREATE VIEW mysecview6 WITH (invalid_option) -- Error + AS SELECT * FROM tbl1 WHERE a < 100; +SELECT relname, relkind, reloptions FROM pg_class + WHERE oid in ('mysecview1'::regclass, 'mysecview2'::regclass, + 'mysecview3'::regclass, 'mysecview4'::regclass); +-- reloptions should be preserved +CREATE OR REPLACE VIEW mysecview1 + AS SELECT * FROM tbl1 WHERE a = 256; +CREATE OR REPLACE VIEW mysecview2 + AS SELECT * FROM tbl1 WHERE a > 256; +-- reloptions should be turned on/off +CREATE OR REPLACE VIEW mysecview3 WITH (security_barrier=true) + AS SELECT * FROM tbl1 WHERE a < 256; +CREATE OR REPLACE VIEW mysecview4 WITH (security_barrier=false) + AS SELECT * FROM tbl1 WHERE a <> 256; +SELECT relname, relkind, reloptions FROM pg_class + WHERE oid in ('mysecview1'::regclass, 'mysecview2'::regclass, + 'mysecview3'::regclass, 'mysecview4'::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..a1968b1 100644 --- a/src/test/regress/sql/select_views.sql +++ b/src/test/regress/sql/select_views.sql @@ -8,3 +8,114 @@ 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 TABLE customer ( + cid int primary key, + name text not null, + tel text, + passwd text +); + +CREATE TABLE credit_card ( + cid int references customer(cid), + cnum text, + climit int +); + +CREATE TABLE credit_usage ( + cid int references customer(cid), + ymd date, + usage int +); + +INSERT INTO customer + VALUES (101, 'alice', '+81-12-3456-7890', 'passwd123'), + (102, 'bob', '+01-234-567-8901', 'beafsteak'), + (103, 'eve', '+49-8765-43210', 'hamburger'); +INSERT INTO credit_card + VALUES (101, '1111-2222-3333-4444', 4000), + (102, '5555-6666-7777-8888', 3000), + (103, '9801-2345-6789-0123', 2000); +INSERT INTO credit_usage + VALUES (101, '2011-09-15', 120), + (101, '2011-10-05', 90), + (101, '2011-10-18', 110), + (101, '2011-10-21', 200), + (101, '2011-11-10', 80), + (102, '2011-09-22', 300), + (102, '2011-10-12', 120), + (102, '2011-10-28', 200), + (103, '2011-10-15', 480); + +CREATE VIEW my_property_normal AS + SELECT * FROM customer WHERE name = current_user; +CREATE VIEW my_property_secure WITH (security_barrier) AS + SELECT * FROM customer WHERE name = current_user; + +CREATE VIEW my_credit_card_normal AS + SELECT * FROM customer l NATURAL JOIN credit_card r + WHERE l.name = current_user; +CREATE VIEW my_credit_card_secure WITH (security_barrier) AS + SELECT * FROM customer l NATURAL JOIN credit_card r + WHERE l.name = current_user; + +CREATE VIEW my_credit_card_usage_normal AS + SELECT * FROM my_credit_card_secure l NATURAL JOIN credit_usage r; +CREATE VIEW my_credit_card_usage_secure WITH (security_barrier) AS + SELECT * FROM my_credit_card_secure l NATURAL JOIN credit_usage r; + +GRANT SELECT ON my_property_normal TO public; +GRANT SELECT ON my_property_secure TO public; +GRANT SELECT ON my_credit_card_normal TO public; +GRANT SELECT ON my_credit_card_secure TO public; +GRANT SELECT ON my_credit_card_usage_normal TO public; +GRANT SELECT ON my_credit_card_usage_secure TO public; + +-- +-- Run leaky view scenarios +-- +SET SESSION AUTHORIZATION alice; + +-- +-- scenario: if a qualifier with tiny-cost is given, it shall be launched +-- prior to the security policy of the view. +-- +SELECT * FROM my_property_normal WHERE f_leak(passwd); +EXPLAIN (COSTS OFF) SELECT * FROM my_property_normal WHERE f_leak(passwd); + +SELECT * FROM my_property_secure WHERE f_leak(passwd); +EXPLAIN (COSTS OFF) SELECT * FROM my_property_secure WHERE f_leak(passwd); + +-- +-- scenario: if a qualifier references only one-side of a particular join- +-- tree, it shall be distributed to the most deep scan plan as +-- possible as we can. +-- +SELECT * FROM my_credit_card_normal WHERE f_leak(cnum); +EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_normal WHERE f_leak(cnum); + +SELECT * FROM my_credit_card_secure WHERE f_leak(cnum); +EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_secure WHERE f_leak(cnum); + +-- +-- scenario: an external qualifier can be pushed-down by in-front-of the +-- views with "security_barrier" attribute +-- +SELECT * FROM my_credit_card_usage_normal + WHERE f_leak(cnum) AND ymd >= '2011-10-01' AND ymd < '2011-11-01'; +EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_usage_normal + WHERE f_leak(cnum) AND ymd >= '2011-10-01' AND ymd < '2011-11-01'; + +SELECT * FROM my_credit_card_usage_secure + WHERE f_leak(cnum) AND ymd >= '2011-10-01' AND ymd < '2011-11-01'; +EXPLAIN (COSTS OFF) SELECT * FROM my_credit_card_usage_secure + WHERE f_leak(cnum) AND ymd >= '2011-10-01' AND ymd < '2011-11-01';