Fix row level security checks during view maintenance

The view maintenance is performed under the view owner privilege.
If a modified table has a RLS policy, the policy must be applied
to relation for the pre-update-state table and the delta table
that contained inserted or deleted tuples. Previously, the security
quals were set to each ENR in a subquery that represents such
relation. However, the security check on the delta table was not
properly handled, and this caused that rows that must not be
accessed from the view owner could appear in the view contents
when the view was refreshed incrementally during a query containing
multiple types of commands, like a modifying CTE that contains
INSERT and UPDATE, or a MERGE command.

This patch fixes it by setting RLS policy to a subquery that
presents the pre-update-state table and the delta able instead of
to each RLS. Also, this change makes the code more simple and easy
to maintain.

CVE-2023-22847
This commit is contained in:
Yugo Nagata 2023-02-14 01:47:05 +09:00
parent 0587e78651
commit 99a176ab97
3 changed files with 93 additions and 107 deletions

View file

@ -932,7 +932,7 @@ SELECT * FROM mv_cte ORDER BY i,j,k;
(8 rows)
ROLLBACK;
-- nested CTE
-- nested CTE
BEGIN;
SELECT create_immv('mv_ivm_nested_cte', 'WITH v AS ( WITH a AS (SELECT * FROM mv_base_a) SELECT i, a.j, b.k FROM mv_base_b b INNER JOIN a USING(i)) SELECT * FROM v');
NOTICE: could not create an index on immv "mv_ivm_nested_cte" automatically
@ -1291,13 +1291,14 @@ SELECT create_immv('mv_ivm_only_values1', 'values(1)');
ERROR: VALUES is not supported on incrementally maintainable materialized view
SELECT create_immv('mv_ivm_only_values2', 'SELECT * FROM (values(1)) AS tmp');
ERROR: VALUES is not supported on incrementally maintainable materialized view
-- base table which has row level security
-- views containing base tables with Row Level Security
DROP USER IF EXISTS ivm_admin;
NOTICE: role "ivm_admin" does not exist, skipping
DROP USER IF EXISTS ivm_user;
NOTICE: role "ivm_user" does not exist, skipping
CREATE USER ivm_admin;
CREATE USER ivm_user;
--- create a table with RLS
SET SESSION AUTHORIZATION ivm_admin;
CREATE TABLE rls_tbl(id int, data text, owner name);
INSERT INTO rls_tbl VALUES
@ -1308,12 +1309,15 @@ INSERT INTO num_tbl VALUES
(1,'one'),
(2,'two'),
(3,'three'),
(4,'four');
(4,'four'),
(5,'five'),
(6,'six');
--- Users can access only their own rows
CREATE POLICY rls_tbl_policy ON rls_tbl FOR SELECT TO PUBLIC USING(owner = current_user);
CREATE POLICY rls_tbl_policy2 ON rls_tbl FOR INSERT TO PUBLIC WITH CHECK(current_user LIKE 'ivm_%');
ALTER TABLE rls_tbl ENABLE ROW LEVEL SECURITY;
GRANT ALL on rls_tbl TO PUBLIC;
GRANT ALL on num_tbl TO PUBLIC;
--- create a view owned by ivm_user
SET SESSION AUTHORIZATION ivm_user;
SELECT create_immv('ivm_rls', 'SELECT * FROM rls_tbl');
NOTICE: could not create an index on immv "ivm_rls" automatically
@ -1330,6 +1334,8 @@ SELECT id, data, owner FROM ivm_rls ORDER BY 1,2,3;
1 | foo | ivm_user
(1 row)
RESET SESSION AUTHORIZATION;
--- inserts rows owned by different users
INSERT INTO rls_tbl VALUES
(3,'baz','ivm_user'),
(4,'qux','postgres');
@ -1340,13 +1346,32 @@ SELECT id, data, owner FROM ivm_rls ORDER BY 1,2,3;
3 | baz | ivm_user
(2 rows)
--- combination of diffent kinds of commands
WITH
i AS (INSERT INTO rls_tbl VALUES(5,'quux','postgres'), (6,'corge','ivm_user')),
u AS (UPDATE rls_tbl SET owner = 'postgres' WHERE id = 1),
u2 AS (UPDATE rls_tbl SET owner = 'ivm_user' WHERE id = 2)
SELECT;
--
(1 row)
SELECT id, data, owner FROM ivm_rls ORDER BY 1,2,3;
id | data | owner
----+-------+----------
2 | bar | ivm_user
3 | baz | ivm_user
6 | corge | ivm_user
(3 rows)
---
SET SESSION AUTHORIZATION ivm_user;
SELECT create_immv('ivm_rls2', 'SELECT * FROM rls_tbl JOIN num_tbl USING(id)');
NOTICE: could not create an index on immv "ivm_rls2" automatically
DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause.
HINT: Create an index on the immv for efficient incremental maintenance.
create_immv
-------------
2
3
(1 row)
RESET SESSION AUTHORIZATION;
@ -1360,9 +1385,10 @@ SELECT;
SELECT * FROM ivm_rls2 ORDER BY 1,2,3;
id | data | owner | num
----+-------+----------+---------
1 | foo | ivm_user | one
2 | bar | ivm_user | two
3 | baz_2 | ivm_user | three_2
(2 rows)
6 | corge | ivm_user | six
(3 rows)
DROP TABLE rls_tbl CASCADE;
NOTICE: drop cascades to 2 other objects

129
matview.c
View file

@ -208,8 +208,6 @@ static void mv_HashPreparedPlan(MV_QueryKey *key, SPIPlanPtr plan);
static void mv_BuildQueryKey(MV_QueryKey *key, Oid matview_id, int32 query_type);
static void clean_up_IVM_hash_entry(MV_TriggerHashEntry *entry, bool is_abort);
static List *get_securityQuals(Oid relId, int rt_index, Query *query);
/* SQL callable functions */
PG_FUNCTION_INFO_V1(IVM_immediate_before);
PG_FUNCTION_INFO_V1(IVM_immediate_maintenance);
@ -1159,7 +1157,31 @@ rewrite_query_for_preupdate_state(Query *query, List *tables,
*/
if (r->relid == table->table_id)
{
lfirst(lc) = get_prestate_rte(r, table, pstate->p_queryEnv, matviewid);
List *securityQuals;
List *withCheckOptions;
bool hasRowSecurity;
bool hasSubLinks;
RangeTblEntry *rte_pre = get_prestate_rte(r, table, pstate->p_queryEnv, matviewid);
/*
* Set a row security poslicies of the modified table to the subquery RTE which
* represents the pre-update state of the table.
*/
get_row_security_policies(query, table->original_rte, i,
&securityQuals, &withCheckOptions,
&hasRowSecurity, &hasSubLinks);
if (hasRowSecurity)
{
query->hasRowSecurity = true;
rte_pre->security_barrier = true;
}
if (hasSubLinks)
query->hasSubLinks = true;
rte_pre->securityQuals = securityQuals;
lfirst(lc) = rte_pre;
table->rte_paths = lappend(table->rte_paths, lappend_int(list_copy(rte_path), i));
break;
}
@ -1212,8 +1234,6 @@ register_delta_ENRs(ParseState *pstate, Query *query, List *tables)
nsitem = addRangeTableEntryForENR(pstate, makeRangeVar(NULL, enr->md.name, -1), true);
rte = nsitem->p_rte;
/* if base table has RLS, set security condition to enr */
rte->securityQuals = get_securityQuals(table->table_id, list_length(query->rtable) + 1, query);
query->rtable = lappend(query->rtable, rte);
table->old_rtes = lappend(table->old_rtes, rte);
@ -1239,8 +1259,6 @@ register_delta_ENRs(ParseState *pstate, Query *query, List *tables)
nsitem = addRangeTableEntryForENR(pstate, makeRangeVar(NULL, enr->md.name, -1), true);
rte = nsitem->p_rte;
/* if base table has RLS, set security condition to enr*/
rte->securityQuals = get_securityQuals(table->table_id, list_length(query->rtable) + 1, query);
query->rtable = lappend(query->rtable, rte);
table->new_rtes = lappend(table->new_rtes, rte);
@ -1348,27 +1366,6 @@ get_prestate_rte(RangeTblEntry *rte, MV_TriggerTable *table,
#endif
sub = transformStmt(pstate, raw->stmt);
/* If this query has setOperations, RTEs in rtables has a subquery which contains ENR */
if (sub->setOperations != NULL)
{
ListCell *lc;
/* add securityQuals for tuplestores */
foreach (lc, sub->rtable)
{
RangeTblEntry *rte;
RangeTblEntry *sub_rte;
rte = (RangeTblEntry *)lfirst(lc);
Assert(rte->subquery != NULL);
sub_rte = (RangeTblEntry *)linitial(rte->subquery->rtable);
if (sub_rte->rtekind == RTE_NAMEDTUPLESTORE)
/* rt_index is always 1, bacause subquery has enr_rte only */
sub_rte->securityQuals = get_securityQuals(sub_rte->relid, 1, sub);
}
}
/* save the original RTE */
table->original_rte = copyObject(rte);
@ -1413,8 +1410,8 @@ make_delta_enr_name(const char *prefix, Oid relid, int count)
/*
* union_ENRs
*
* Make a single table delta by unionning all transition tables of the modified table
* whose RTE is specified by
* Replace RTE of the modified table with a single table delta that combine its
* all transition tables.
*/
static RangeTblEntry*
union_ENRs(RangeTblEntry *rte, Oid relid, List *enr_rtes, const char *prefix,
@ -1425,7 +1422,9 @@ union_ENRs(RangeTblEntry *rte, Oid relid, List *enr_rtes, const char *prefix,
RawStmt *raw;
Query *sub;
int i;
RangeTblEntry *enr_rte;
/* the previous RTE must be a subquery which represents "pre-state" table */
Assert(rte->rtekind == RTE_SUBQUERY);
/* Create a ParseState for rewriting the view definition query */
pstate = make_parsestate(NULL);
@ -1451,26 +1450,13 @@ union_ENRs(RangeTblEntry *rte, Oid relid, List *enr_rtes, const char *prefix,
#endif
sub = transformStmt(pstate, raw->stmt);
rte->rtekind = RTE_SUBQUERY;
/*
* Update the subquery so that it represent the combined transition
* table. Note that we leave the security_barrier and securityQuals
* fields so that the subquery relation can be protected by the RLS
* policy as same as the modified table.
*/
rte->subquery = sub;
rte->security_barrier = false;
/* Clear fields that should not be set in a subquery RTE */
rte->relid = InvalidOid;
rte->relkind = 0;
rte->rellockmode = 0;
rte->tablesample = NULL;
rte->inh = false; /* must not be set for a subquery */
rte->requiredPerms = 0; /* no permission check on subquery itself */
rte->checkAsUser = InvalidOid;
rte->selectedCols = NULL;
rte->insertedCols = NULL;
rte->updatedCols = NULL;
rte->extraUpdatedCols = NULL;
/* if base table has RLS, set security condition to enr*/
enr_rte = (RangeTblEntry *)linitial(sub->rtable);
/* rt_index is always 1, bacause subquery has enr_rte only */
enr_rte->securityQuals = get_securityQuals(relid, 1, sub);
return rte;
}
@ -2960,46 +2946,3 @@ isIvmName(const char *s)
return (strncmp(s, "__ivm_", 6) == 0);
return false;
}
/*
* get_securityQuals
*
* Get row security policy on a relation.
* This is used by IVM for copying RLS from base table to enr.
*/
static List *
get_securityQuals(Oid relId, int rt_index, Query *query)
{
ParseState *pstate;
Relation rel;
ParseNamespaceItem *nsitem;
RangeTblEntry *rte;
List *securityQuals;
List *withCheckOptions;
bool hasRowSecurity;
bool hasSubLinks;
securityQuals = NIL;
pstate = make_parsestate(NULL);
rel = table_open(relId, NoLock);
nsitem = addRangeTableEntryForRelation(pstate, rel, AccessShareLock, NULL, false, false);
rte = nsitem->p_rte;
get_row_security_policies(query, rte, rt_index,
&securityQuals, &withCheckOptions,
&hasRowSecurity, &hasSubLinks);
/*
* Make sure the query is marked correctly if row level security
* applies, or if the new quals had sublinks.
*/
if (hasRowSecurity)
query->hasRowSecurity = true;
if (hasSubLinks)
query->hasSubLinks = true;
table_close(rel, NoLock);
return securityQuals;
}

View file

@ -318,7 +318,7 @@ SELECT;
SELECT * FROM mv_cte ORDER BY i,j,k;
ROLLBACK;
-- nested CTE
-- nested CTE
BEGIN;
SELECT create_immv('mv_ivm_nested_cte', 'WITH v AS ( WITH a AS (SELECT * FROM mv_base_a) SELECT i, a.j, b.k FROM mv_base_b b INNER JOIN a USING(i)) SELECT * FROM v');
WITH
@ -522,13 +522,14 @@ SELECT create_immv('mv_ivm_only_values1', 'values(1)');
SELECT create_immv('mv_ivm_only_values2', 'SELECT * FROM (values(1)) AS tmp');
-- base table which has row level security
-- views containing base tables with Row Level Security
DROP USER IF EXISTS ivm_admin;
DROP USER IF EXISTS ivm_user;
CREATE USER ivm_admin;
CREATE USER ivm_user;
SET SESSION AUTHORIZATION ivm_admin;
--- create a table with RLS
SET SESSION AUTHORIZATION ivm_admin;
CREATE TABLE rls_tbl(id int, data text, owner name);
INSERT INTO rls_tbl VALUES
(1,'foo','ivm_user'),
@ -538,23 +539,39 @@ INSERT INTO num_tbl VALUES
(1,'one'),
(2,'two'),
(3,'three'),
(4,'four');
(4,'four'),
(5,'five'),
(6,'six');
--- Users can access only their own rows
CREATE POLICY rls_tbl_policy ON rls_tbl FOR SELECT TO PUBLIC USING(owner = current_user);
CREATE POLICY rls_tbl_policy2 ON rls_tbl FOR INSERT TO PUBLIC WITH CHECK(current_user LIKE 'ivm_%');
ALTER TABLE rls_tbl ENABLE ROW LEVEL SECURITY;
GRANT ALL on rls_tbl TO PUBLIC;
GRANT ALL on num_tbl TO PUBLIC;
--- create a view owned by ivm_user
SET SESSION AUTHORIZATION ivm_user;
SELECT create_immv('ivm_rls', 'SELECT * FROM rls_tbl');
SELECT id, data, owner FROM ivm_rls ORDER BY 1,2,3;
RESET SESSION AUTHORIZATION;
--- inserts rows owned by different users
INSERT INTO rls_tbl VALUES
(3,'baz','ivm_user'),
(4,'qux','postgres');
SELECT id, data, owner FROM ivm_rls ORDER BY 1,2,3;
SELECT create_immv('ivm_rls2', 'SELECT * FROM rls_tbl JOIN num_tbl USING(id)');
--- combination of diffent kinds of commands
WITH
i AS (INSERT INTO rls_tbl VALUES(5,'quux','postgres'), (6,'corge','ivm_user')),
u AS (UPDATE rls_tbl SET owner = 'postgres' WHERE id = 1),
u2 AS (UPDATE rls_tbl SET owner = 'ivm_user' WHERE id = 2)
SELECT;
SELECT id, data, owner FROM ivm_rls ORDER BY 1,2,3;
---
SET SESSION AUTHORIZATION ivm_user;
SELECT create_immv('ivm_rls2', 'SELECT * FROM rls_tbl JOIN num_tbl USING(id)');
RESET SESSION AUTHORIZATION;
WITH