diff --git a/doc/src/sgml/ref/alter_view.sgml b/doc/src/sgml/ref/alter_view.sgml index 00f4ecb..d1f3a54 100644 --- a/doc/src/sgml/ref/alter_view.sgml +++ b/doc/src/sgml/ref/alter_view.sgml @@ -142,6 +142,16 @@ ALTER VIEW [ IF EXISTS ] name RESET + + security_definer (boolean) + + + Changes the security-definer property of the view. The value must + be Boolean value, such as true + or false. + + + diff --git a/doc/src/sgml/ref/create_view.sgml b/doc/src/sgml/ref/create_view.sgml index e37bb00..71c6f79 100644 --- a/doc/src/sgml/ref/create_view.sgml +++ b/doc/src/sgml/ref/create_view.sgml @@ -147,6 +147,16 @@ CREATE VIEW name AS WITH RECURSIVE name ( + + + security_definer (boolean) + + + This should be used if the view is intended to be executed with + owner privileges rather than the current user. + + + @@ -276,6 +286,37 @@ CREATE VIEW vista AS SELECT text 'Hello World' AS hello; to replace it (this includes being a member of the owning role). + + Security definer Views + + + security definer views + + + + Security definer views uses the view owner id instead of the current user + in the following conditions, otherwise the current user is used to verify + the privileges and etc. + + + + The view is used in INSERT, UPDATE + and DELETE statements in the same way as on a + regular table. + + + + + + To apply row-level security policies on the underlying base + relations of the view, based on the security definer, the + corresponding policies related to the user are applied. + + + + + + Updatable Views diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c index 931dceb..430ea71 100644 --- a/src/backend/access/common/reloptions.c +++ b/src/backend/access/common/reloptions.c @@ -89,6 +89,15 @@ static relopt_bool boolRelOpts[] = }, false }, + { + { + "security_definer", + "specifies that the view is to be executed with the privileges of the user that created it.", + RELOPT_KIND_VIEW, + AccessExclusiveLock + }, + false + }, /* list terminator */ {{NULL}} }; @@ -1320,6 +1329,8 @@ view_reloptions(Datum reloptions, bool validate) static const relopt_parse_elt tab[] = { {"security_barrier", RELOPT_TYPE_BOOL, offsetof(ViewOptions, security_barrier)}, + { "security_definer", RELOPT_TYPE_BOOL, + offsetof(ViewOptions, security_definer) }, {"check_option", RELOPT_TYPE_STRING, offsetof(ViewOptions, check_option_offset)} }; diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c index 315d00c..ea5edaf 100644 --- a/src/backend/rewrite/rewriteHandler.c +++ b/src/backend/rewrite/rewriteHandler.c @@ -24,6 +24,7 @@ #include "catalog/pg_type.h" #include "commands/trigger.h" #include "foreign/fdwapi.h" +#include "miscadmin.h" #include "nodes/makefuncs.h" #include "nodes/nodeFuncs.h" #include "parser/analyze.h" @@ -1616,6 +1617,7 @@ fireRIRrules(Query *parsetree, List *activeRIRs, bool forUpdatePushedDown) int origResultRelation = parsetree->resultRelation; int rt_index; ListCell *lc; + bool security_definer_view = false; /* * don't try to convert this into a foreach loop, because rtable list can @@ -1777,18 +1779,27 @@ fireRIRrules(Query *parsetree, List *activeRIRs, bool forUpdatePushedDown) ++rt_index; /* Only normal relations can have RLS policies */ - if (rte->rtekind != RTE_RELATION || - rte->relkind != RELKIND_RELATION) + if (rte->rtekind != RTE_RELATION) continue; - + rel = heap_open(rte->relid, NoLock); + if (rte->relkind == RELKIND_VIEW && RelationIsSecurityDefinerView(rel)) + security_definer_view = true; + + if (rte->relkind != RELKIND_RELATION) + { + heap_close(rel, NoLock); + continue; + } + /* * Fetch any new security quals that must be applied to this RTE. */ get_row_security_policies(parsetree, rte, rt_index, &securityQuals, &withCheckOptions, - &hasRowSecurity, &hasSubLinks); + &hasRowSecurity, &hasSubLinks, + security_definer_view); if (securityQuals != NIL || withCheckOptions != NIL) { @@ -2834,7 +2845,10 @@ rewriteTargetView(Query *parsetree, Relation view) * the executor still performs appropriate permissions checks for the * query caller's use of the view. */ - new_rte->checkAsUser = view->rd_rel->relowner; + if (RelationIsSecurityDefinerView(view)) + new_rte->checkAsUser = view->rd_rel->relowner; + else + new_rte->checkAsUser = GetUserId(); new_rte->requiredPerms = view_rte->requiredPerms; /* diff --git a/src/backend/rewrite/rowsecurity.c b/src/backend/rewrite/rowsecurity.c index eebc493..513a2dd 100644 --- a/src/backend/rewrite/rowsecurity.c +++ b/src/backend/rewrite/rowsecurity.c @@ -105,7 +105,8 @@ row_security_policy_hook_type row_security_policy_hook_restrictive = NULL; void get_row_security_policies(Query *root, RangeTblEntry *rte, int rt_index, List **securityQuals, List **withCheckOptions, - bool *hasRowSecurity, bool *hasSubLinks) + bool *hasRowSecurity, bool *hasSubLinks, + bool security_definer_view) { Oid user_id; int rls_status; @@ -125,10 +126,13 @@ get_row_security_policies(Query *root, RangeTblEntry *rte, int rt_index, return; /* Switch to checkAsUser if it's set */ - user_id = rte->checkAsUser ? rte->checkAsUser : GetUserId(); + if (security_definer_view) + user_id = rte->checkAsUser ? rte->checkAsUser : GetUserId(); + else + user_id = GetUserId(); /* Determine the state of RLS for this, pass checkAsUser explicitly */ - rls_status = check_enable_rls(rte->relid, rte->checkAsUser, false); + rls_status = check_enable_rls(rte->relid, user_id, false); /* If there is no RLS on this table at all, nothing to do */ if (rls_status == RLS_NONE) diff --git a/src/include/rewrite/rowsecurity.h b/src/include/rewrite/rowsecurity.h index 4af244d..0aabf3d 100644 --- a/src/include/rewrite/rowsecurity.h +++ b/src/include/rewrite/rowsecurity.h @@ -43,6 +43,7 @@ extern PGDLLIMPORT row_security_policy_hook_type row_security_policy_hook_restri extern void get_row_security_policies(Query *root, RangeTblEntry *rte, int rt_index, List **securityQuals, List **withCheckOptions, - bool *hasRowSecurity, bool *hasSubLinks); + bool *hasRowSecurity, bool *hasSubLinks, + bool security_definer_view); #endif /* ROWSECURITY_H */ diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h index 8a55a09..da248e3 100644 --- a/src/include/utils/rel.h +++ b/src/include/utils/rel.h @@ -265,6 +265,7 @@ typedef struct ViewOptions { int32 vl_len_; /* varlena header (do not touch directly!) */ bool security_barrier; + bool security_definer; int check_option_offset; } ViewOptions; @@ -278,6 +279,14 @@ typedef struct ViewOptions ((ViewOptions *) (relation)->rd_options)->security_barrier : false) /* + * RelationIsSecurityDefiner + * Returns whether the relation is security definer, or not. + */ +#define RelationIsSecurityDefinerView(relation) \ + ((relation)->rd_options ? \ + ((ViewOptions *) (relation)->rd_options)->security_definer : false) + +/* * RelationHasCheckOption * Returns true if the relation is a view defined with either the local * or the cascaded check option. Note multiple eval of argument! diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out index 8d925dc..d1f5742 100644 --- a/src/test/regress/expected/rowsecurity.out +++ b/src/test/regress/expected/rowsecurity.out @@ -1425,7 +1425,7 @@ CREATE POLICY p1 ON b1 USING (a % 2 = 0); ALTER TABLE b1 ENABLE ROW LEVEL SECURITY; GRANT ALL ON b1 TO rls_regress_user1; SET SESSION AUTHORIZATION rls_regress_user1; -CREATE VIEW bv1 WITH (security_barrier) AS SELECT * FROM b1 WHERE a > 0 WITH CHECK OPTION; +CREATE VIEW bv1 WITH (security_barrier, security_definer) AS SELECT * FROM b1 WHERE a > 0 WITH CHECK OPTION; GRANT ALL ON bv1 TO rls_regress_user2; SET SESSION AUTHORIZATION rls_regress_user2; EXPLAIN (COSTS OFF) SELECT * FROM bv1 WHERE f_leak(b); @@ -1892,7 +1892,7 @@ EXPLAIN (COSTS OFF) EXECUTE plancache_test3; -- -- View and Table owner are the same. SET SESSION AUTHORIZATION rls_regress_user0; -CREATE VIEW rls_view AS SELECT * FROM z1 WHERE f_leak(b); +CREATE VIEW rls_view WITH (security_definer) AS SELECT * FROM z1 WHERE f_leak(b); GRANT SELECT ON rls_view TO rls_regress_user1; -- Query as role that is not owner of view or table. Should return all records. SET SESSION AUTHORIZATION rls_regress_user1; @@ -1941,7 +1941,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM rls_view; DROP VIEW rls_view; -- View and Table owners are different. SET SESSION AUTHORIZATION rls_regress_user1; -CREATE VIEW rls_view AS SELECT * FROM z1 WHERE f_leak(b); +CREATE VIEW rls_view WITH (security_definer) AS SELECT * FROM z1 WHERE f_leak(b); GRANT SELECT ON rls_view TO rls_regress_user0; -- Query as role that is not owner of view but is owner of table. -- Should return records based on view owner policies. diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out index 6c71371..b62f1ad 100644 --- a/src/test/regress/expected/updatable_views.out +++ b/src/test/regress/expected/updatable_views.out @@ -966,7 +966,7 @@ CREATE USER view_user2; SET SESSION AUTHORIZATION view_user1; CREATE TABLE base_tbl(a int, b text, c float); INSERT INTO base_tbl VALUES (1, 'Row 1', 1.0); -CREATE VIEW rw_view1 AS SELECT b AS bb, c AS cc, a AS aa FROM base_tbl; +CREATE VIEW rw_view1 WITH (security_definer) AS SELECT b AS bb, c AS cc, a AS aa FROM base_tbl; INSERT INTO rw_view1 VALUES ('Row 2', 2.0, 2); GRANT SELECT ON base_tbl TO view_user2; GRANT SELECT ON rw_view1 TO view_user2; diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql index decde90..817ade7 100644 --- a/src/test/regress/sql/rowsecurity.sql +++ b/src/test/regress/sql/rowsecurity.sql @@ -491,7 +491,7 @@ ALTER TABLE b1 ENABLE ROW LEVEL SECURITY; GRANT ALL ON b1 TO rls_regress_user1; SET SESSION AUTHORIZATION rls_regress_user1; -CREATE VIEW bv1 WITH (security_barrier) AS SELECT * FROM b1 WHERE a > 0 WITH CHECK OPTION; +CREATE VIEW bv1 WITH (security_barrier, security_definer) AS SELECT * FROM b1 WHERE a > 0 WITH CHECK OPTION; GRANT ALL ON bv1 TO rls_regress_user2; SET SESSION AUTHORIZATION rls_regress_user2; @@ -687,7 +687,7 @@ EXPLAIN (COSTS OFF) EXECUTE plancache_test3; -- -- View and Table owner are the same. SET SESSION AUTHORIZATION rls_regress_user0; -CREATE VIEW rls_view AS SELECT * FROM z1 WHERE f_leak(b); +CREATE VIEW rls_view WITH (security_definer) AS SELECT * FROM z1 WHERE f_leak(b); GRANT SELECT ON rls_view TO rls_regress_user1; -- Query as role that is not owner of view or table. Should return all records. @@ -703,7 +703,7 @@ DROP VIEW rls_view; -- View and Table owners are different. SET SESSION AUTHORIZATION rls_regress_user1; -CREATE VIEW rls_view AS SELECT * FROM z1 WHERE f_leak(b); +CREATE VIEW rls_view WITH (security_definer) AS SELECT * FROM z1 WHERE f_leak(b); GRANT SELECT ON rls_view TO rls_regress_user0; -- Query as role that is not owner of view but is owner of table. diff --git a/src/test/regress/sql/updatable_views.sql b/src/test/regress/sql/updatable_views.sql index 5297a71..8452c18 100644 --- a/src/test/regress/sql/updatable_views.sql +++ b/src/test/regress/sql/updatable_views.sql @@ -397,7 +397,7 @@ CREATE USER view_user2; SET SESSION AUTHORIZATION view_user1; CREATE TABLE base_tbl(a int, b text, c float); INSERT INTO base_tbl VALUES (1, 'Row 1', 1.0); -CREATE VIEW rw_view1 AS SELECT b AS bb, c AS cc, a AS aa FROM base_tbl; +CREATE VIEW rw_view1 WITH (security_definer) AS SELECT b AS bb, c AS cc, a AS aa FROM base_tbl; INSERT INTO rw_view1 VALUES ('Row 2', 2.0, 2); GRANT SELECT ON base_tbl TO view_user2;