diff --git a/doc/src/sgml/ref/alter_view.sgml b/doc/src/sgml/ref/alter_view.sgml index c3839057c0705..e78176bce8074 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 ( view_option_name [= view_option_value] [, ... ] ) +ALTER VIEW name RESET ( view_option_name [, ... ] ) @@ -102,6 +104,24 @@ ALTER VIEW name SET SCHEMA + + + view_option_name + + + The name of a view option to be set or reset. + + + + + + view_option_name + + + The new value for a view option. + + + diff --git a/doc/src/sgml/ref/create_view.sgml b/doc/src/sgml/ref/create_view.sgml index 417f8c38e7eeb..6e86840866248 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 ( view_option_name [= view_option_value] [, ... ] ) ] AS query @@ -98,6 +99,18 @@ CREATE [ OR REPLACE ] [ TEMP | TEMPORARY ] VIEW n + + WITH ( view_option_name [= view_option_value] [, ... ] ) + + + This clause specifies optional parameters for a view; currently, the + only suppored parameter name is security_barrier, + which should be enabled when a view is intended to provide row-level + security. See for full details. + + + + query diff --git a/doc/src/sgml/rules.sgml b/doc/src/sgml/rules.sgml index 1b06519b38015..896cc64ecd2f1 100644 --- a/doc/src/sgml/rules.sgml +++ b/doc/src/sgml/rules.sgml @@ -1822,8 +1822,9 @@ GRANT SELECT ON phone_number TO secretary; Note that while views can be used to hide the contents of certain columns using the technique shown above, they cannot be used to reliably - conceal the data in unseen rows. For example, the following view is - insecure: + conceal the data in unseen rows unless the + security_barrier flag has been set. For example, + the following view is insecure: CREATE VIEW phone_number AS SELECT person, phone FROM phone_data WHERE phone NOT LIKE '412%'; @@ -1870,6 +1871,40 @@ SELECT * FROM phone_number WHERE tricky(person, phone); which references shoelace_log is an unqualified INSERT. This might not be true in more complex scenarios. + + + When it is necessary for a view to provide row-level security, the + security_barrier attribute should be applied to + the view. This prevents maliciously-chosen functions and operators from + being invoked on rows until afterthe view has done its work. For + example, if the view shown above had been created like this, it would + be secure: + +CREATE VIEW phone_number WITH (security_barrier) AS + SELECT person, phone FROM phone_data WHERE phone NOT LIKE '412%'; + + Views created with the security_barrier may perform + far worse than views created without this option. In general, there is + no way to avoid this: the fastest possible plan must be rejected + if it may compromise security. For this reason, this option is not + enabled by default. + + + + It is important to understand that even a view created with the + security_barrier option is intended to be secure only + in the limited sense that the contents of the invisible tuples will not + passed to possibly-insecure functions. The user may well have other means + of making inferences about the unseen data; for example, they can see the + query plan using EXPLAIN, or measure the runtime of + queries against the view. A malicious attacker might be able to infer + something about the amount of unseen data, or even gain some information + about the data distribution or most common values (since these things may + affect the runtime of the plan; or even, since they are also reflected in + the optimizer statistics, the choice of plan). If these types of "covert + channel" attacks are of concern, it is probably unwise to grant any access + to the data at all. + diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c index 100172fa4ac37..7220e0e0ce96a 100644 --- a/src/backend/access/common/reloptions.c +++ b/src/backend/access/common/reloptions.c @@ -67,6 +67,14 @@ static relopt_bool boolRelOpts[] = }, true }, + { + { + "security_barrier", + "View acts as a row security barrier", + RELOPT_KIND_VIEW + }, + false + }, /* list terminator */ {{NULL}} }; @@ -781,6 +789,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; @@ -1139,7 +1148,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); @@ -1159,7 +1170,7 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind) } /* - * Parse options for heaps and toast tables. + * Parse options for heaps, views and toast tables. */ bytea * heap_reloptions(char relkind, Datum reloptions, bool validate) @@ -1181,6 +1192,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 8473c9e7f4452..4953def029b67 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -366,7 +366,9 @@ static void ATExecDropCluster(Relation rel, LOCKMODE lockmode); static void ATPrepSetTableSpace(AlteredTableInfo *tab, Relation rel, char *tablespacename, LOCKMODE lockmode); static void ATExecSetTableSpace(Oid tableOid, Oid newTableSpace, LOCKMODE lockmode); -static void ATExecSetRelOptions(Relation rel, List *defList, bool isReset, LOCKMODE lockmode); +static void ATExecSetRelOptions(Relation rel, List *defList, + AlterTableType operation, + LOCKMODE lockmode); static void ATExecEnableDisableTrigger(Relation rel, char *trigname, char fires_when, bool skip_system, LOCKMODE lockmode); static void ATExecEnableDisableRule(Relation rel, char *rulename, @@ -2866,6 +2868,7 @@ AlterTableGetLockLevel(List *cmds) case AT_DropCluster: case AT_SetRelOptions: case AT_ResetRelOptions: + case AT_ReplaceRelOptions: case AT_SetOptions: case AT_ResetOptions: case AT_SetStorage: @@ -3094,8 +3097,9 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd, pass = AT_PASS_MISC; /* doesn't actually matter */ break; case AT_SetRelOptions: /* SET (...) */ - case AT_ResetRelOptions: /* RESET (...) */ - ATSimplePermissions(rel, ATT_TABLE | ATT_INDEX); + case AT_ResetRelOptions: /* RESET (...) */ + case AT_ReplaceRelOptions: /* reset them all, then set just these */ + ATSimplePermissions(rel, ATT_TABLE | ATT_INDEX | ATT_VIEW); /* This command never recurses */ /* No command-specific prep needed */ pass = AT_PASS_MISC; @@ -3338,12 +3342,10 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab, Relation rel, */ break; case AT_SetRelOptions: /* SET (...) */ - ATExecSetRelOptions(rel, (List *) cmd->def, false, lockmode); - break; case AT_ResetRelOptions: /* RESET (...) */ - ATExecSetRelOptions(rel, (List *) cmd->def, true, lockmode); + case AT_ReplaceRelOptions: /* replace entire option list */ + ATExecSetRelOptions(rel, (List *) cmd->def, cmd->subtype, lockmode); break; - case AT_EnableTrig: /* ENABLE TRIGGER name */ ATExecEnableDisableTrigger(rel, cmd->name, TRIGGER_FIRES_ON_ORIGIN, false, lockmode); @@ -8271,10 +8273,11 @@ ATPrepSetTableSpace(AlteredTableInfo *tab, Relation rel, char *tablespacename, L } /* - * ALTER TABLE/INDEX SET (...) or RESET (...) + * Set, reset, or replace reloptions. */ static void -ATExecSetRelOptions(Relation rel, List *defList, bool isReset, LOCKMODE lockmode) +ATExecSetRelOptions(Relation rel, List *defList, AlterTableType operation, + LOCKMODE lockmode) { Oid relid; Relation pgclass; @@ -8288,28 +8291,44 @@ ATExecSetRelOptions(Relation rel, List *defList, bool isReset, LOCKMODE lockmode bool repl_repl[Natts_pg_class]; static char *validnsps[] = HEAP_RELOPT_NAMESPACES; - if (defList == NIL) + if (defList == NIL && operation != AT_ReplaceRelOptions) return; /* nothing to do */ pgclass = heap_open(RelationRelationId, RowExclusiveLock); - /* Get the old reloptions */ + /* Fetch heap tuple */ relid = RelationGetRelid(rel); tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relid)); if (!HeapTupleIsValid(tuple)) elog(ERROR, "cache lookup failed for relation %u", relid); - datum = SysCacheGetAttr(RELOID, tuple, Anum_pg_class_reloptions, &isnull); + if (operation == AT_ReplaceRelOptions) + { + /* + * If we're supposed to replace the reloptions list, we just pretend + * there were none before. + */ + datum = (Datum) 0; + isnull = true; + } + else + { + /* Get the old reloptions */ + datum = SysCacheGetAttr(RELOID, tuple, Anum_pg_class_reloptions, + &isnull); + } /* Generate new proposed reloptions (text array) */ newOptions = transformRelOptions(isnull ? (Datum) 0 : datum, - defList, NULL, validnsps, false, isReset); + defList, NULL, validnsps, false, + operation == AT_ResetRelOptions); /* Validate */ switch (rel->rd_rel->relkind) { case RELKIND_RELATION: case RELKIND_TOASTVALUE: + case RELKIND_VIEW: (void) heap_reloptions(rel->rd_rel->relkind, newOptions, true); break; case RELKIND_INDEX: @@ -8357,15 +8376,30 @@ ATExecSetRelOptions(Relation rel, List *defList, bool isReset, LOCKMODE lockmode toastrel = heap_open(toastid, lockmode); - /* Get the old reloptions */ + /* Fetch heap tuple */ tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(toastid)); if (!HeapTupleIsValid(tuple)) elog(ERROR, "cache lookup failed for relation %u", toastid); - datum = SysCacheGetAttr(RELOID, tuple, Anum_pg_class_reloptions, &isnull); + if (operation == AT_ReplaceRelOptions) + { + /* + * If we're supposed to replace the reloptions list, we just + * pretend there were none before. + */ + datum = (Datum) 0; + isnull = true; + } + else + { + /* Get the old reloptions */ + datum = SysCacheGetAttr(RELOID, tuple, Anum_pg_class_reloptions, + &isnull); + } newOptions = transformRelOptions(isnull ? (Datum) 0 : datum, - defList, "toast", validnsps, false, isReset); + defList, "toast", validnsps, false, + operation == AT_ResetRelOptions); (void) heap_reloptions(RELKIND_TOASTVALUE, newOptions, true); diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c index b23819965869a..3edcd33eaf4c1 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 ... @@ -203,6 +206,15 @@ DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace, descriptor = BuildDescForRelation(attrList); checkViewTupleDesc(descriptor, rel->rd_att); + /* + * The new options list replaces the existing options list, even + * if it's empty. + */ + atcmd = makeNode(AlterTableCmd); + atcmd->subtype = AT_ReplaceRelOptions; + atcmd->def = (Node *) options; + atcmds = lappend(atcmds, atcmd); + /* * If new attributes have been added, we must add pg_attribute entries * for them. It is convenient (although overkill) to use the ALTER @@ -210,14 +222,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,9 +237,11 @@ DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace, atcmd->def = (Node *) lfirst(c); atcmds = lappend(atcmds, atcmd); } - AlterTableInternal(viewOid, atcmds, true); } + /* OK, let's do it. */ + AlterTableInternal(viewOid, atcmds, true); + /* * Seems okay, so return the OID of the pre-existing view. */ @@ -250,7 +261,8 @@ DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace, createStmt->tableElts = attrList; createStmt->inhRelations = NIL; createStmt->constraints = NIL; - createStmt->options = list_make1(defWithOids(false)); + createStmt->options = options; + createStmt->options = lappend(options, defWithOids(false)); createStmt->oncommit = ONCOMMIT_NOOP; createStmt->tablespacename = NULL; createStmt->if_not_exists = false; @@ -513,7 +525,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/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c index 7178b529a2e18..dd2dd25ed1b7b 100644 --- a/src/backend/nodes/copyfuncs.c +++ b/src/backend/nodes/copyfuncs.c @@ -1965,6 +1965,7 @@ _copyRangeTblEntry(const RangeTblEntry *from) COPY_SCALAR_FIELD(relid); COPY_SCALAR_FIELD(relkind); COPY_NODE_FIELD(subquery); + COPY_SCALAR_FIELD(security_barrier); COPY_SCALAR_FIELD(jointype); COPY_NODE_FIELD(joinaliasvars); COPY_NODE_FIELD(funcexpr); diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c index 9f7daf4eda2f2..c2fdb2ba365cb 100644 --- a/src/backend/nodes/equalfuncs.c +++ b/src/backend/nodes/equalfuncs.c @@ -2228,6 +2228,7 @@ _equalRangeTblEntry(const RangeTblEntry *a, const RangeTblEntry *b) COMPARE_SCALAR_FIELD(relid); COMPARE_SCALAR_FIELD(relkind); COMPARE_NODE_FIELD(subquery); + COMPARE_SCALAR_FIELD(security_barrier); COMPARE_SCALAR_FIELD(jointype); COMPARE_NODE_FIELD(joinaliasvars); COMPARE_NODE_FIELD(funcexpr); diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index bef1e78f5a2d1..cdc2cab55c611 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -2321,6 +2321,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); + WRITE_BOOL_FIELD(security_barrier); break; case RTE_JOIN: WRITE_ENUM_FIELD(jointype, JoinType); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index 3de20ad93a96a..fa9ad164234df 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -1192,6 +1192,7 @@ _readRangeTblEntry(void) break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); + READ_BOOL_FIELD(security_barrier); break; case RTE_JOIN: READ_ENUM_FIELD(jointype, JoinType); diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c index 815b996a131d8..b3777eae2e404 100644 --- a/src/backend/optimizer/path/allpaths.c +++ b/src/backend/optimizer/path/allpaths.c @@ -744,6 +744,11 @@ set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel, * pseudoconstant clauses; better to have the gating node above the * subquery. * + * Also, if the sub-query has "security_barrier" flag, it means the + * sub-query originated from a view that must enforce row-level security. + * We must not push down quals in order to avoid information leaks, either + * via side-effects or error output. + * * Non-pushed-down clauses will get evaluated as qpquals of the * SubqueryScan node. * @@ -762,7 +767,16 @@ set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel, RestrictInfo *rinfo = (RestrictInfo *) lfirst(l); Node *clause = (Node *) rinfo->clause; + /* + * XXX. You might wonder why we're testing rte->security_barrier + * qual-by-qual here rather than hoisting the test up into the + * surrounding if statement; after all, the answer will be the + * same for all quals. The answer is that we expect to shortly + * change this logic to allow pushing down some quals that use only + * "leakproof" operators even through a security barrier. + */ if (!rinfo->pseudoconstant && + !rte->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 8bb011b711658..a711c4f646b5a 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) && + !rte->security_barrier && (containing_appendrel == NULL || is_safe_append_member(rte->subquery))) return pull_up_simple_subquery(root, jtnode, rte, @@ -712,6 +713,7 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte, * pull_up_subqueries. */ if (is_simple_subquery(subquery) && + !rte->security_barrier && (containing_appendrel == NULL || is_safe_append_member(subquery))) { /* good to go */ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 7e8f39abdf708..8943c5b7d042b 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -7322,26 +7322,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 a6f4141dd7b67..8448226bfc84c 100644 --- a/src/backend/rewrite/rewriteHandler.c +++ b/src/backend/rewrite/rewriteHandler.c @@ -1382,6 +1382,7 @@ ApplyRetrieveRule(Query *parsetree, rte->rtekind = RTE_SUBQUERY; rte->relid = InvalidOid; + rte->security_barrier = RelationIsSecurityView(relation); rte->subquery = rule_action; rte->inh = false; /* must not be set for a subquery */ diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c index abef8abc3826e..bb411f9ad1d08 100644 --- a/src/backend/utils/adt/selfuncs.c +++ b/src/backend/utils/adt/selfuncs.c @@ -4372,6 +4372,19 @@ examine_simple_variable(PlannerInfo *root, Var *var, subquery->distinctClause) return; + /* + * If the sub-query originated from a view with the security_barrier + * attribute, we treat it as a black-box from outside of the view. + * This is probably a harsher restriction than necessary; it's + * certainly OK for the selectivity estimator (which is a C function, + * and therefore omnipotent anyway) to look at the statistics. But + * many selectivity estimators will happily *invoke the operator + * function* to try to work out a good estimate - and that's not OK. + * So for now, we do this. + */ + if (rte->security_barrier) + return; + /* * OK, fetch RelOptInfo for subquery. Note that we don't change the * rel returned in vardata, since caller expects it to be a rel of the diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c index f9ad75e7f8901..cfb48d3ec4c32 100644 --- a/src/backend/utils/cache/relcache.c +++ b/src/backend/utils/cache/relcache.c @@ -374,6 +374,7 @@ RelationParseRelOptions(Relation relation, HeapTuple tuple) case RELKIND_RELATION: case RELKIND_TOASTVALUE: case RELKIND_INDEX: + case RELKIND_VIEW: break; default: return; diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index 5deb9d658d8ec..89a8a23599fe3 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -12397,8 +12397,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 10b2f9ea4db09..afba016195a1f 100644 --- a/src/include/access/reloptions.h +++ b/src/include/access/reloptions.h @@ -43,8 +43,9 @@ typedef enum relopt_kind RELOPT_KIND_ATTRIBUTE = (1 << 6), RELOPT_KIND_TABLESPACE = (1 << 7), RELOPT_KIND_SPGIST = (1 << 8), + RELOPT_KIND_VIEW = (1 << 9), /* if you add a new kind, make sure you update "last_default" too */ - RELOPT_KIND_LAST_DEFAULT = RELOPT_KIND_SPGIST, + 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 734227366d18f..6e8b11027d495 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -706,6 +706,7 @@ typedef struct RangeTblEntry * Fields valid for a subquery RTE (else NULL): */ Query *subquery; /* the sub-query */ + bool security_barrier; /* subquery from security_barrier view */ /* * Fields valid for a join RTE (else NULL/zero): @@ -1208,6 +1209,7 @@ typedef enum AlterTableType AT_SetTableSpace, /* SET TABLESPACE */ AT_SetRelOptions, /* SET (...) -- AM specific parameters */ AT_ResetRelOptions, /* RESET (...) -- AM specific parameters */ + AT_ReplaceRelOptions, /* replace reloption list in its entirety */ AT_EnableTrig, /* ENABLE TRIGGER name */ AT_EnableAlwaysTrig, /* ENABLE ALWAYS TRIGGER name */ AT_EnableReplicaTrig, /* ENABLE REPLICA TRIGGER name */ @@ -2277,6 +2279,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/utils/rel.h b/src/include/utils/rel.h index 70d16eb01e42c..ed9c003857123 100644 --- a/src/include/utils/rel.h +++ b/src/include/utils/rel.h @@ -195,6 +195,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; /* for views */ } StdRdOptions; #define HEAP_MIN_FILLFACTOR 10 @@ -222,6 +223,14 @@ typedef struct StdRdOptions #define RelationGetTargetPageFreeSpace(relation, defaultff) \ (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 f9490a3a55062..cc93854c42308 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) + ORDER BY relname; + relname | relkind | reloptions +------------+---------+-------------------------- + mysecview1 | v | + mysecview2 | v | {security_barrier=true} + mysecview3 | v | {security_barrier=false} + mysecview4 | v | {security_barrier=true} +(4 rows) + +CREATE OR REPLACE VIEW mysecview1 + AS SELECT * FROM tbl1 WHERE a = 256; +CREATE OR REPLACE VIEW mysecview2 + AS SELECT * FROM tbl1 WHERE a > 256; +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) + ORDER BY relname; + relname | relkind | reloptions +------------+---------+-------------------------- + mysecview1 | v | + mysecview2 | v | + 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/sql/create_view.sql b/src/test/regress/sql/create_view.sql index 86cfc5162c695..48d8d22d1b8e4 100644 --- a/src/test/regress/sql/create_view.sql +++ b/src/test/regress/sql/create_view.sql @@ -191,6 +191,39 @@ 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) + ORDER BY relname; + +CREATE OR REPLACE VIEW mysecview1 + AS SELECT * FROM tbl1 WHERE a = 256; +CREATE OR REPLACE VIEW mysecview2 + AS SELECT * FROM tbl1 WHERE a > 256; +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) + ORDER BY relname; + DROP SCHEMA temp_view_test CASCADE; DROP SCHEMA testviewschm2 CASCADE;