more RLS oversights
I happened to notice this morning while hacking that the
"hasRowSecurity" fields added to PlannerGlobal and PlannedStmt have
not been given proper nodefuncs.c support. Both need to be added to
outfuncs.c, and the latter to copyfuncs.c. The latter omission may
well be a security bug, although I haven't attempted to verify that,
but fortunately this isn't released yet.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Robert,
* Robert Haas (robertmhaas@gmail.com) wrote:
I happened to notice this morning while hacking that the
"hasRowSecurity" fields added to PlannerGlobal and PlannedStmt have
not been given proper nodefuncs.c support. Both need to be added to
outfuncs.c, and the latter to copyfuncs.c. The latter omission may
well be a security bug, although I haven't attempted to verify that,
but fortunately this isn't released yet.
I saw this and will address it. Would be great if you wouldn't mind
CC'ing me directly on anything RLS-related, same as you CC'd me on the
column-privilege backpatch. I expect I'll probably notice anyway, but
I'll see them faster when I'm CC'd.
I agree that it's great that we're catching issues prior to when the
feature is released and look forward to anything else you (or anyone
else!) finds.
Thanks!
Stephen
Robert, all,
* Stephen Frost (sfrost@snowman.net) wrote:
* Robert Haas (robertmhaas@gmail.com) wrote:
I happened to notice this morning while hacking that the
"hasRowSecurity" fields added to PlannerGlobal and PlannedStmt have
not been given proper nodefuncs.c support. Both need to be added to
outfuncs.c, and the latter to copyfuncs.c. The latter omission may
well be a security bug, although I haven't attempted to verify that,
but fortunately this isn't released yet.I saw this and will address it. Would be great if you wouldn't mind
CC'ing me directly on anything RLS-related, same as you CC'd me on the
column-privilege backpatch. I expect I'll probably notice anyway, but
I'll see them faster when I'm CC'd.I agree that it's great that we're catching issues prior to when the
feature is released and look forward to anything else you (or anyone
else!) finds.
I've pushed a fix for this. Please let me know if you see any other
issues or run into any problems.
Thanks!
Stephen
On Wed, Feb 25, 2015 at 11:37:24PM -0500, Stephen Frost wrote:
* Stephen Frost (sfrost@snowman.net) wrote:
I agree that it's great that we're catching issues prior to when the
feature is released and look forward to anything else you (or anyone
else!) finds.I've pushed a fix for this. Please let me know if you see any other
issues or run into any problems.
(1) CreatePolicy() and AlterPolicy() omit to call assign_expr_collations() on
the node trees. Test case:
begin;
set row_security = force;
create table t (c) as values ('bar'::text);
create policy p on t using (c < ('foo'::text COLLATE "C"));
alter table t enable row level security;
table pg_policy; -- note ":inputcollid 0"
select * from t; -- ERROR: could not determine which collation ...
rollback;
(2) CreatePolicy() and AlterPolicy() omit to create a pg_shdepend entry for
each role in the TO clause. Test case:
begin;
create role alice;
create table t (c) as values ('bar'::text);
grant select on table t to alice;
create policy p on t to alice using (true);
select refclassid::regclass, refobjid, refobjsubid, deptype
from pg_depend where classid = 'pg_policy'::regclass;
select refclassid::regclass, refobjid, deptype
from pg_shdepend where classid = 'pg_policy'::regclass;
savepoint q; drop role alice; rollback to q;
revoke all on table t from alice;
\d t
drop role alice;
\d t
rollback;
+static void +dumpPolicy(Archive *fout, PolicyInfo *polinfo)
...
+ if (polinfo->polqual != NULL) + appendPQExpBuffer(query, " USING %s", polinfo->polqual);
(3) The USING clause needs parentheses; a dump+reload failed like so:
pg_restore: [archiver (db)] could not execute query: ERROR: syntax error at or near "CASE"
LINE 2: CASE
^
Command was: CREATE POLICY "p2" ON "category" FOR ALL TO PUBLIC USING
CASE
WHEN ("current_user"() = 'rls_regress_user1'::"name") THE...
Add the same parentheses to psql \d output also, keeping that output similar
to the SQL syntax.
(3a) I found this by hacking the rowsecurity.sql regression test to not drop
its objects, then running the pg_upgrade test suite. New features that affect
pg_dump should leave objects in the regression database to test the pg_dump
support via that suite.
(4) When DefineQueryRewrite() is about to convert a table to a view, it checks
the table for features unavailable to views. For example, it rejects tables
having triggers. It omits to reject tables having relrowsecurity or a
pg_policy record. Test case:
begin;
set row_security = force;
create table t (c) as select * from generate_series(1,5);
create policy p on t using (c % 2 = 1);
alter table t enable row level security;
table t;
truncate t;
create rule "_RETURN" as on select to t do instead
select * from generate_series(1,5) t0(c);
table t;
select polrelid::regclass from pg_policy;
select relrowsecurity from pg_class where oid = 't'::regclass;
rollback;
+ <para> + Referential integrity checks, such as unique or primary key constraints + and foreign key references, will bypass row security to ensure that + data integrity is maintained. Care must be taken when developing + schemas and row level policies to avoid a "covert channel" leak of + information through these referential integrity checks.
...
+ /* + * Row-level security should be disabled in the case where a foreign-key + * relation is queried to check existence of tuples that references the + * primary-key being modified. + */ + temp_sec_context = save_sec_context | SECURITY_LOCAL_USERID_CHANGE; + if (qkey->constr_queryno == RI_PLAN_CHECK_LOOKUPPK + || qkey->constr_queryno == RI_PLAN_CHECK_LOOKUPPK_FROM_PK + || qkey->constr_queryno == RI_PLAN_RESTRICT_DEL_CHECKREF + || qkey->constr_queryno == RI_PLAN_RESTRICT_UPD_CHECKREF) + temp_sec_context |= SECURITY_ROW_LEVEL_DISABLED;
(5) This code does not use SECURITY_ROW_LEVEL_DISABLED for foreign keys with
CASCADE, SET NULL or SET DEFAULT actions. The associated documentation says
nothing about this presumably-important distinction.
Is SECURITY_ROW_LEVEL_DISABLED mode even needed? We switch users to the owner
of the FROM-clause table before running an RI query. That means use of this
mode can only matter under row_security=force, right? I would rest easier if
this mode went away, because it is a security landmine. If user code managed
to run in this mode, it would bypass every policy in the database. (I find no
such vulnerability today, because we use the mode only for parse analysis of
ri_triggers.c queries.)
(6) AlterPolicy() calls InvokeObjectPostAlterHook(PolicyRelationId, ...), but
CreatePolicy() and DropPolicy() lack their respective hook invocations.
(7) Using an aggregate function in a policy predicate elicits an inapposite
error message due to use of EXPR_KIND_WHERE for parse analysis. Need a new
ParseExprKind. Test case:
begin;
set row_security = force;
create table t (c) as values ('bar'::text);
-- ERROR: aggregate functions are not allowed in WHERE
create policy p on t using (max(c));
rollback;
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Noah,
* Noah Misch (noah@leadboat.com) wrote:
On Wed, Feb 25, 2015 at 11:37:24PM -0500, Stephen Frost wrote:
* Stephen Frost (sfrost@snowman.net) wrote:
I agree that it's great that we're catching issues prior to when the
feature is released and look forward to anything else you (or anyone
else!) finds.I've pushed a fix for this. Please let me know if you see any other
issues or run into any problems.(1) CreatePolicy() and AlterPolicy() omit to call assign_expr_collations() on
the node trees. Test case:
Will fix.
(2) CreatePolicy() and AlterPolicy() omit to create a pg_shdepend entry for
each role in the TO clause. Test case:
Will fix.
+static void +dumpPolicy(Archive *fout, PolicyInfo *polinfo)...
+ if (polinfo->polqual != NULL) + appendPQExpBuffer(query, " USING %s", polinfo->polqual);(3) The USING clause needs parentheses; a dump+reload failed like so:
Will fix.
Add the same parentheses to psql \d output also, keeping that output similar
to the SQL syntax.
Yup.
(3a) I found this by hacking the rowsecurity.sql regression test to not drop
its objects, then running the pg_upgrade test suite. New features that affect
pg_dump should leave objects in the regression database to test the pg_dump
support via that suite.
Will fix.
(4) When DefineQueryRewrite() is about to convert a table to a view, it checks
the table for features unavailable to views. For example, it rejects tables
having triggers. It omits to reject tables having relrowsecurity or a
pg_policy record. Test case:
Will fix.
+ <para> + Referential integrity checks, such as unique or primary key constraints + and foreign key references, will bypass row security to ensure that + data integrity is maintained. Care must be taken when developing + schemas and row level policies to avoid a "covert channel" leak of + information through these referential integrity checks....
+ /* + * Row-level security should be disabled in the case where a foreign-key + * relation is queried to check existence of tuples that references the + * primary-key being modified. + */ + temp_sec_context = save_sec_context | SECURITY_LOCAL_USERID_CHANGE; + if (qkey->constr_queryno == RI_PLAN_CHECK_LOOKUPPK + || qkey->constr_queryno == RI_PLAN_CHECK_LOOKUPPK_FROM_PK + || qkey->constr_queryno == RI_PLAN_RESTRICT_DEL_CHECKREF + || qkey->constr_queryno == RI_PLAN_RESTRICT_UPD_CHECKREF) + temp_sec_context |= SECURITY_ROW_LEVEL_DISABLED;(5) This code does not use SECURITY_ROW_LEVEL_DISABLED for foreign keys with
CASCADE, SET NULL or SET DEFAULT actions. The associated documentation says
nothing about this presumably-important distinction.
Agreed, will fix.
Is SECURITY_ROW_LEVEL_DISABLED mode even needed? We switch users to the owner
of the FROM-clause table before running an RI query. That means use of this
mode can only matter under row_security=force, right? I would rest easier if
this mode went away, because it is a security landmine. If user code managed
to run in this mode, it would bypass every policy in the database. (I find no
such vulnerability today, because we use the mode only for parse analysis of
ri_triggers.c queries.)
That's a very interesting point.. At first blush, I agree, it shouldn't
be necessary. I'll play with it and see if I can get everything to work
properly without it.
(6) AlterPolicy() calls InvokeObjectPostAlterHook(PolicyRelationId, ...), but
CreatePolicy() and DropPolicy() lack their respective hook invocations.
Will fix.
(7) Using an aggregate function in a policy predicate elicits an inapposite
error message due to use of EXPR_KIND_WHERE for parse analysis. Need a new
ParseExprKind. Test case:
Will fix.
Thanks a lot for your review! Much appreciated.
Stephen
On 07/03/2015 10:03 AM, Noah Misch wrote:
(1) CreatePolicy() and AlterPolicy() omit to call assign_expr_collations() on
the node trees. Test case:begin;
set row_security = force;
create table t (c) as values ('bar'::text);
create policy p on t using (c < ('foo'::text COLLATE "C"));
alter table t enable row level security;
table pg_policy; -- note ":inputcollid 0"
select * from t; -- ERROR: could not determine which collation ...
rollback;
The attached fixes this issue for me, but I am unsure whether we really
need/want the regression test. Given the recent push to increase test
coverage maybe so. Any objections?
Joe
Attachments:
rls-assign_expr_collations.difftext/x-diff; name=rls-assign_expr_collations.diffDownload
diff --git a/src/backend/commands/policy.c b/src/backend/commands/policy.c
index 11efc9f..7232983 100644
*** a/src/backend/commands/policy.c
--- b/src/backend/commands/policy.c
*************** CreatePolicy(CreatePolicyStmt *stmt)
*** 538,543 ****
--- 538,547 ----
EXPR_KIND_WHERE,
"POLICY");
+ /* Fix up collation information */
+ assign_expr_collations(qual_pstate, qual);
+ assign_expr_collations(with_check_pstate, with_check_qual);
+
/* Open pg_policy catalog */
pg_policy_rel = heap_open(PolicyRelationId, RowExclusiveLock);
*************** AlterPolicy(AlterPolicyStmt *stmt)
*** 681,686 ****
--- 685,693 ----
EXPR_KIND_WHERE,
"POLICY");
+ /* Fix up collation information */
+ assign_expr_collations(qual_pstate, qual);
+
qual_parse_rtable = qual_pstate->p_rtable;
free_parsestate(qual_pstate);
}
*************** AlterPolicy(AlterPolicyStmt *stmt)
*** 701,706 ****
--- 708,716 ----
EXPR_KIND_WHERE,
"POLICY");
+ /* Fix up collation information */
+ assign_expr_collations(with_check_pstate, with_check_qual);
+
with_check_parse_rtable = with_check_pstate->p_rtable;
free_parsestate(with_check_pstate);
}
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 4073c1b..eabfd93 100644
*** a/src/test/regress/expected/rowsecurity.out
--- b/src/test/regress/expected/rowsecurity.out
*************** ERROR: permission denied for relation c
*** 2730,2735 ****
--- 2730,2756 ----
RESET SESSION AUTHORIZATION;
DROP TABLE copy_t;
--
+ -- Collation support
+ --
+ BEGIN;
+ SET row_security = force;
+ CREATE TABLE coll_t (c) AS VALUES ('bar'::text);
+ CREATE POLICY coll_p ON coll_t USING (c < ('foo'::text COLLATE "C"));
+ ALTER TABLE coll_t ENABLE ROW LEVEL SECURITY;
+ SELECT (string_to_array(polqual, ':'))[7] AS inputcollid FROM pg_policy WHERE polrelid = 'coll_t'::regclass;
+ inputcollid
+ ------------------
+ inputcollid 950
+ (1 row)
+
+ SELECT * FROM coll_t;
+ c
+ -----
+ bar
+ (1 row)
+
+ ROLLBACK;
+ --
-- Clean up objects
--
RESET SESSION AUTHORIZATION;
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
index fdd9b89..782824a 100644
*** a/src/test/regress/sql/rowsecurity.sql
--- b/src/test/regress/sql/rowsecurity.sql
*************** RESET SESSION AUTHORIZATION;
*** 1088,1093 ****
--- 1088,1105 ----
DROP TABLE copy_t;
--
+ -- Collation support
+ --
+ BEGIN;
+ SET row_security = force;
+ CREATE TABLE coll_t (c) AS VALUES ('bar'::text);
+ CREATE POLICY coll_p ON coll_t USING (c < ('foo'::text COLLATE "C"));
+ ALTER TABLE coll_t ENABLE ROW LEVEL SECURITY;
+ SELECT (string_to_array(polqual, ':'))[7] AS inputcollid FROM pg_policy WHERE polrelid = 'coll_t'::regclass;
+ SELECT * FROM coll_t;
+ ROLLBACK;
+
+ --
-- Clean up objects
--
RESET SESSION AUTHORIZATION;
On Fri, Jul 10, 2015 at 03:08:53PM -0700, Joe Conway wrote:
On 07/03/2015 10:03 AM, Noah Misch wrote:
(1) CreatePolicy() and AlterPolicy() omit to call assign_expr_collations() on
the node trees.
The attached fixes this issue for me, but I am unsure whether we really
need/want the regression test. Given the recent push to increase test
coverage maybe so.
I wouldn't remove the test from your patch.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
On 07/10/2015 06:15 PM, Noah Misch wrote:
On Fri, Jul 10, 2015 at 03:08:53PM -0700, Joe Conway wrote:
On 07/03/2015 10:03 AM, Noah Misch wrote:
(1) CreatePolicy() and AlterPolicy() omit to call
assign_expr_collations() on the node trees.The attached fixes this issue for me, but I am unsure whether we
really need/want the regression test. Given the recent push to
increase test coverage maybe so.I wouldn't remove the test from your patch.
Ok -- pushed.
- --
Joe Conway
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v2.0.22 (GNU/Linux)
iQIcBAEBAgAGBQJVoYj2AAoJEDfy90M199hleBcP/3anI8IIEkFyPiUDX0QvzfEM
EOBm+AdolwgAvscU6RaDbrVXJlE32YbSWsXhtXOA5jJvhY80ln3YO+ko1ONWV3dW
iYbvSO+zQalHDqmID2bqbnY/k+7GTGWPCTsSLMsmUK7P0QCVP2f02lCukqr4yWpH
tIVbOfb0A1+Mrb9dxta43Bj32maBBiEpWIwaebotik6BmfwHNeeaZ082PUJQvaqS
wtshrlctAaCsCyjQnNiPvtD9mw0rlSWOhNDc7R8KGflWnwXmBlyu7jD4aFHKcPZO
v1ErqG2Z0allm3p6snpFbiunQssVpHgF7V8FcWxIReu73lV25ig3Ix56MoUXHIOq
a2Y5iAfRw106V1GA6ARW0kjCaE0DrRcfA6/Um8LeEhw44cvUBZkhXx/ozt0t62pz
6mvhKN4UjmO/XfbA9GEN7b9kDz+LZtMFQ1PqcH7mK3OYKgGfYTAdJOA7qwHuWMBC
MGVHP5WEUCJEToTNyzVe0matOH8+IHS4LQ9qAtUFVCmhh27FK0m8kjoZAmT/xDk5
uNcMG9mvBOTZe5EmVdC1gywDOsRntzAgWM1SFBK2v0YEgj3YarKll839Jm+dNHGZ
nxjniR/XJkNxISrTN6Qq797nhYsmpqRg+d7ZVk0GxmhfNNp/f2SFRVB9/9ovrk4x
//7pllazs48qu6e/3eYK
=EEZ7
-----END PGP SIGNATURE-----
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
On 07/03/2015 10:03 AM, Noah Misch wrote:
(2) CreatePolicy() and AlterPolicy() omit to create a pg_shdepend
entry for each role in the TO clause. Test case:
Please see the attached patch. Note that I used SHARED_DEPENDENCY_ACL
for this. It seems appropriate, but possibly we should invent a new
shared dependency type for this use case? Comments?
Thanks,
Joe
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v2.0.22 (GNU/Linux)
iQIcBAEBAgAGBQJVtSlAAAoJEDfy90M199hlrkIP/0BqMj30JpJVj3H5pIopU0RZ
pesBzFzzsWmt2QmasX9hajRfo6eXQAWPmKQmw/NDTJvHHNACxLdo0MHE7A9y7No7
aZIFTm0KhnG4jpzxfzpGqQ4ron5Vsc2TlNQgCBYCtbEEtuD0mV2qmcuTkqGte7iB
7iqneRBhmTYBy63X7S0ir4AhDi+JJg8P4F7YuU/XMcha5v5CpNJAToPpW7mCoZ8O
9w/RbXCHso7p1DIxfx4tfxVwLQ7j7G2j0rXbuA2e6OKfwZWWrk5E8Wnvc3xy3yCY
J37fcjOxFdhU/j1j+Tr90LYOSuRu5fQ4z6PmmsPkBM3T0Vllz/nNP64aVKbmHzga
szrIBERRs2icKa4OI08qZF42ObIEv0/t/NuyrXpegY6awRNNNsjyZvwM+51zdjw1
fuWh+2rObzh3RDH8lPB0N0rVfDMM+wU85Vaekckv/7UQ/pbWcwsYD/tykA1engQ8
Ww8kBAEct4dWdqppTqvLLxLgo4d+vuiS1mJ7SRY2aZFRX8QQ8TfB3eObETUsU4/X
9UWyMn+E0Au9ICX+wfD4whaBKst8EuO36qOZx0oZt+++EMOlzypgkCCxDtZO0KWd
KZHbtmJXHFVH+ihbEGXAKMIx+gLqSDG3IKy9MIJxpB3JY3XSdBNqobL2hiQy36/w
svK7i+mEYmUCQ6pB1j8c
=P1AS
-----END PGP SIGNATURE-----
Attachments:
20150726.00-rls-shdep.patchtext/x-diff; name=20150726.00-rls-shdep.patchDownload
diff --git a/src/backend/commands/policy.c b/src/backend/commands/policy.c
index 17b48d4..20ac54e 100644
*** a/src/backend/commands/policy.c
--- b/src/backend/commands/policy.c
***************
*** 22,27 ****
--- 22,28 ----
#include "catalog/indexing.h"
#include "catalog/namespace.h"
#include "catalog/objectaccess.h"
+ #include "catalog/pg_authid.h"
#include "catalog/pg_policy.h"
#include "catalog/pg_type.h"
#include "commands/policy.h"
***************
*** 48,54 ****
static void RangeVarCallbackForPolicy(const RangeVar *rv,
Oid relid, Oid oldrelid, void *arg);
static char parse_policy_command(const char *cmd_name);
! static ArrayType *policy_role_list_to_array(List *roles);
/*
* Callback to RangeVarGetRelidExtended().
--- 49,55 ----
static void RangeVarCallbackForPolicy(const RangeVar *rv,
Oid relid, Oid oldrelid, void *arg);
static char parse_policy_command(const char *cmd_name);
! static Datum *policy_role_list_to_array(List *roles, int *num_roles);
/*
* Callback to RangeVarGetRelidExtended().
*************** parse_policy_command(const char *cmd_nam
*** 130,159 ****
/*
* policy_role_list_to_array
! * helper function to convert a list of RoleSpecs to an array of role ids.
*/
! static ArrayType *
! policy_role_list_to_array(List *roles)
{
! ArrayType *role_ids;
! Datum *temp_array;
ListCell *cell;
- int num_roles;
int i = 0;
/* Handle no roles being passed in as being for public */
if (roles == NIL)
{
! temp_array = (Datum *) palloc(sizeof(Datum));
! temp_array[0] = ObjectIdGetDatum(ACL_ID_PUBLIC);
! role_ids = construct_array(temp_array, 1, OIDOID, sizeof(Oid), true,
! 'i');
! return role_ids;
}
! num_roles = list_length(roles);
! temp_array = (Datum *) palloc(num_roles * sizeof(Datum));
foreach(cell, roles)
{
--- 131,158 ----
/*
* policy_role_list_to_array
! * helper function to convert a list of RoleSpecs to an array of
! * role id Datums.
*/
! static Datum *
! policy_role_list_to_array(List *roles, int *num_roles)
{
! Datum *role_oids;
ListCell *cell;
int i = 0;
/* Handle no roles being passed in as being for public */
if (roles == NIL)
{
! *num_roles = 1;
! role_oids = (Datum *) palloc(*num_roles * sizeof(Datum));
! role_oids[0] = ObjectIdGetDatum(ACL_ID_PUBLIC);
! return role_oids;
}
! *num_roles = list_length(roles);
! role_oids = (Datum *) palloc(*num_roles * sizeof(Datum));
foreach(cell, roles)
{
*************** policy_role_list_to_array(List *roles)
*** 164,187 ****
*/
if (spec->roletype == ROLESPEC_PUBLIC)
{
! if (num_roles != 1)
ereport(WARNING,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("ignoring roles specified other than public"),
errhint("All roles are members of the public role.")));
! temp_array[0] = ObjectIdGetDatum(ACL_ID_PUBLIC);
! num_roles = 1;
! break;
}
else
! temp_array[i++] =
ObjectIdGetDatum(get_rolespec_oid((Node *) spec, false));
}
! role_ids = construct_array(temp_array, num_roles, OIDOID, sizeof(Oid), true,
! 'i');
!
! return role_ids;
}
/*
--- 163,187 ----
*/
if (spec->roletype == ROLESPEC_PUBLIC)
{
! Datum *tmp_role_oids;
!
! if (*num_roles != 1)
ereport(WARNING,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("ignoring roles specified other than public"),
errhint("All roles are members of the public role.")));
! *num_roles = 1;
! tmp_role_oids = (Datum *) palloc(*num_roles * sizeof(Datum));
! tmp_role_oids[0] = ObjectIdGetDatum(ACL_ID_PUBLIC);
!
! return tmp_role_oids;
}
else
! role_oids[i++] =
ObjectIdGetDatum(get_rolespec_oid((Node *) spec, false));
}
! return role_oids;
}
/*
*************** CreatePolicy(CreatePolicyStmt *stmt)
*** 463,468 ****
--- 463,470 ----
Relation target_table;
Oid table_id;
char polcmd;
+ Datum *role_oids;
+ int nitems = 0;
ArrayType *role_ids;
ParseState *qual_pstate;
ParseState *with_check_pstate;
*************** CreatePolicy(CreatePolicyStmt *stmt)
*** 476,481 ****
--- 478,484 ----
bool isnull[Natts_pg_policy];
ObjectAddress target;
ObjectAddress myself;
+ int i;
/* Parse command */
polcmd = parse_policy_command(stmt->cmd);
*************** CreatePolicy(CreatePolicyStmt *stmt)
*** 498,506 ****
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("only WITH CHECK expression allowed for INSERT")));
-
/* Collect role ids */
! role_ids = policy_role_list_to_array(stmt->roles);
/* Parse the supplied clause */
qual_pstate = make_parsestate(NULL);
--- 501,510 ----
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("only WITH CHECK expression allowed for INSERT")));
/* Collect role ids */
! role_oids = policy_role_list_to_array(stmt->roles, &nitems);
! role_ids = construct_array(role_oids, nitems, OIDOID,
! sizeof(Oid), true, 'i');
/* Parse the supplied clause */
qual_pstate = make_parsestate(NULL);
*************** CreatePolicy(CreatePolicyStmt *stmt)
*** 614,619 ****
--- 618,635 ----
recordDependencyOnExpr(&myself, with_check_qual,
with_check_pstate->p_rtable, DEPENDENCY_NORMAL);
+ /* Register role dependencies */
+ target.classId = AuthIdRelationId;
+ target.objectSubId = 0;
+ for (i = 0; i < nitems; i++)
+ {
+ target.objectId = DatumGetObjectId(role_oids[i]);
+ /* no dependency if public */
+ if (target.objectId != ACL_ID_PUBLIC)
+ recordSharedDependencyOn(&myself, &target,
+ SHARED_DEPENDENCY_ACL);
+ }
+
/* Invalidate Relation Cache */
CacheInvalidateRelcache(target_table);
*************** AlterPolicy(AlterPolicyStmt *stmt)
*** 641,646 ****
--- 657,664 ----
Oid policy_id;
Relation target_table;
Oid table_id;
+ Datum *role_oids;
+ int nitems = 0;
ArrayType *role_ids = NULL;
List *qual_parse_rtable = NIL;
List *with_check_parse_rtable = NIL;
*************** AlterPolicy(AlterPolicyStmt *stmt)
*** 658,667 ****
Datum cmd_datum;
char polcmd;
bool polcmd_isnull;
/* Parse role_ids */
if (stmt->roles != NULL)
! role_ids = policy_role_list_to_array(stmt->roles);
/* Get id of table. Also handles permissions checks. */
table_id = RangeVarGetRelidExtended(stmt->table, AccessExclusiveLock,
--- 676,690 ----
Datum cmd_datum;
char polcmd;
bool polcmd_isnull;
+ int i;
/* Parse role_ids */
if (stmt->roles != NULL)
! {
! role_oids = policy_role_list_to_array(stmt->roles, &nitems);
! role_ids = construct_array(role_oids, nitems, OIDOID,
! sizeof(Oid), true, 'i');
! }
/* Get id of table. Also handles permissions checks. */
table_id = RangeVarGetRelidExtended(stmt->table, AccessExclusiveLock,
*************** AlterPolicy(AlterPolicyStmt *stmt)
*** 825,830 ****
--- 848,866 ----
recordDependencyOnExpr(&myself, with_check_qual, with_check_parse_rtable,
DEPENDENCY_NORMAL);
+ /* Register role dependencies */
+ deleteSharedDependencyRecordsFor(PolicyRelationId, policy_id, 0);
+ target.classId = AuthIdRelationId;
+ target.objectSubId = 0;
+ for (i = 0; i < nitems; i++)
+ {
+ target.objectId = DatumGetObjectId(role_oids[i]);
+ /* no dependency if public */
+ if (target.objectId != ACL_ID_PUBLIC)
+ recordSharedDependencyOn(&myself, &target,
+ SHARED_DEPENDENCY_ACL);
+ }
+
heap_freetuple(new_tuple);
/* Invalidate Relation Cache */
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index e7c242c..48c85ca 100644
*** a/src/test/regress/expected/rowsecurity.out
--- b/src/test/regress/expected/rowsecurity.out
*************** SELECT * FROM coll_t;
*** 2860,2865 ****
--- 2860,2936 ----
ROLLBACK;
--
+ -- Shared Object Dependencies
+ --
+ RESET SESSION AUTHORIZATION;
+ BEGIN;
+ CREATE ROLE alice;
+ CREATE ROLE bob;
+ CREATE TABLE tbl1 (c) AS VALUES ('bar'::text);
+ GRANT SELECT ON TABLE tbl1 TO alice;
+ CREATE POLICY P ON tbl1 TO alice, bob USING (true);
+ SELECT refclassid::regclass, deptype
+ FROM pg_depend
+ WHERE classid = 'pg_policy'::regclass
+ AND refobjid = 'tbl1'::regclass;
+ refclassid | deptype
+ ------------+---------
+ pg_class | a
+ (1 row)
+
+ SELECT refclassid::regclass, deptype
+ FROM pg_shdepend
+ WHERE classid = 'pg_policy'::regclass
+ AND refobjid IN ('alice'::regrole, 'bob'::regrole);
+ refclassid | deptype
+ ------------+---------
+ pg_authid | a
+ pg_authid | a
+ (2 rows)
+
+ SAVEPOINT q;
+ DROP ROLE alice; --fails due to dependency on POLICY p
+ ERROR: role "alice" cannot be dropped because some objects depend on it
+ DETAIL: privileges for policy p on table tbl1
+ privileges for table tbl1
+ ROLLBACK TO q;
+ ALTER POLICY p ON tbl1 TO bob USING (true);
+ SAVEPOINT q;
+ DROP ROLE alice; --fails due to dependency on GRANT SELECT
+ ERROR: role "alice" cannot be dropped because some objects depend on it
+ DETAIL: privileges for table tbl1
+ ROLLBACK TO q;
+ REVOKE ALL ON TABLE tbl1 FROM alice;
+ SAVEPOINT q;
+ DROP ROLE alice; --succeeds
+ ROLLBACK TO q;
+ SAVEPOINT q;
+ DROP ROLE bob; --fails due to dependency on POLICY p
+ ERROR: role "bob" cannot be dropped because some objects depend on it
+ DETAIL: privileges for policy p on table tbl1
+ ROLLBACK TO q;
+ DROP POLICY p ON tbl1;
+ SAVEPOINT q;
+ DROP ROLE bob; -- succeeds
+ ROLLBACK TO q;
+ SELECT refclassid::regclass, deptype
+ FROM pg_depend
+ WHERE classid = 'pg_policy'::regclass
+ AND refobjid = 'tbl1'::regclass;
+ refclassid | deptype
+ ------------+---------
+ (0 rows)
+
+ SELECT refclassid::regclass, deptype
+ FROM pg_shdepend
+ WHERE classid = 'pg_policy'::regclass
+ AND refobjid IN ('alice'::regrole, 'bob'::regrole);
+ refclassid | deptype
+ ------------+---------
+ (0 rows)
+
+ ROLLBACK; -- cleanup
+ --
-- Clean up objects
--
RESET SESSION AUTHORIZATION;
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
index e86f814..f1f1b6b 100644
*** a/src/test/regress/sql/rowsecurity.sql
--- b/src/test/regress/sql/rowsecurity.sql
*************** SELECT * FROM coll_t;
*** 1153,1158 ****
--- 1153,1211 ----
ROLLBACK;
--
+ -- Shared Object Dependencies
+ --
+ RESET SESSION AUTHORIZATION;
+ BEGIN;
+ CREATE ROLE alice;
+ CREATE ROLE bob;
+ CREATE TABLE tbl1 (c) AS VALUES ('bar'::text);
+ GRANT SELECT ON TABLE tbl1 TO alice;
+ CREATE POLICY P ON tbl1 TO alice, bob USING (true);
+ SELECT refclassid::regclass, deptype
+ FROM pg_depend
+ WHERE classid = 'pg_policy'::regclass
+ AND refobjid = 'tbl1'::regclass;
+ SELECT refclassid::regclass, deptype
+ FROM pg_shdepend
+ WHERE classid = 'pg_policy'::regclass
+ AND refobjid IN ('alice'::regrole, 'bob'::regrole);
+
+ SAVEPOINT q;
+ DROP ROLE alice; --fails due to dependency on POLICY p
+ ROLLBACK TO q;
+
+ ALTER POLICY p ON tbl1 TO bob USING (true);
+ SAVEPOINT q;
+ DROP ROLE alice; --fails due to dependency on GRANT SELECT
+ ROLLBACK TO q;
+
+ REVOKE ALL ON TABLE tbl1 FROM alice;
+ SAVEPOINT q;
+ DROP ROLE alice; --succeeds
+ ROLLBACK TO q;
+
+ SAVEPOINT q;
+ DROP ROLE bob; --fails due to dependency on POLICY p
+ ROLLBACK TO q;
+
+ DROP POLICY p ON tbl1;
+ SAVEPOINT q;
+ DROP ROLE bob; -- succeeds
+ ROLLBACK TO q;
+
+ SELECT refclassid::regclass, deptype
+ FROM pg_depend
+ WHERE classid = 'pg_policy'::regclass
+ AND refobjid = 'tbl1'::regclass;
+ SELECT refclassid::regclass, deptype
+ FROM pg_shdepend
+ WHERE classid = 'pg_policy'::regclass
+ AND refobjid IN ('alice'::regrole, 'bob'::regrole);
+
+ ROLLBACK; -- cleanup
+
+ --
-- Clean up objects
--
RESET SESSION AUTHORIZATION;
Joe Conway wrote:
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1On 07/03/2015 10:03 AM, Noah Misch wrote:
(2) CreatePolicy() and AlterPolicy() omit to create a pg_shdepend
entry for each role in the TO clause. Test case:Please see the attached patch. Note that I used SHARED_DEPENDENCY_ACL
for this. It seems appropriate, but possibly we should invent a new
shared dependency type for this use case? Comments?
Hmm, these are not ACL objects, so conceptually it seems cleaner to use
a different symbol for this. I think the catalog state and the error
messages would be a bit confusing otherwise.
if (spec->roletype == ROLESPEC_PUBLIC)
{
! Datum *tmp_role_oids;
!
! if (*num_roles != 1)
ereport(WARNING,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("ignoring roles specified other than public"),
errhint("All roles are members of the public role.")));
! *num_roles = 1;
! tmp_role_oids = (Datum *) palloc(*num_roles * sizeof(Datum));
! tmp_role_oids[0] = ObjectIdGetDatum(ACL_ID_PUBLIC);
Isn't this leaking the previously allocated array? Not sure it's all
that critical, but still. (I don't think you really need to call palloc
at all here.)
--
�lvaro Herrera http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
On 07/27/2015 01:13 PM, Alvaro Herrera wrote:
Hmm, these are not ACL objects, so conceptually it seems cleaner to
use a different symbol for this. I think the catalog state and the
error messages would be a bit confusing otherwise.
Ok -- done
Isn't this leaking the previously allocated array? Not sure it's
all that critical, but still. (I don't think you really need to
call palloc at all here.)
Agreed -- I think the attached is a bit cleaner.
Any other comments or concerns?
- --
Joe Conway
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v2.0.22 (GNU/Linux)
iQIcBAEBAgAGBQJVts3+AAoJEDfy90M199hlzTIQAJ6LEOrEEhHEjsoVvCT6TUAu
pMQl/LWmb0s3/vF9o5VFTpVd21k0LxcggD+DdbxSagiw1WpLK5x67C9Lj5uuFn/d
3a95nFnQje3ciQJaAKS4XcGyx8+6rHPZqfBRC1rARbLuDHrwpKqmbKwvpQCud4xN
kD7OolYk5Gd3cId0xH0XBHuwLVJg4Bt/MAexrcHn+kXuQoF8V6iOjnmBI/BHvTQy
w4j4ov7DpWSAR1ZiCTCkF2ZuNd9TJ8FmAEtSDVrlWMxB9J+9PU5oTfD50jp62BTz
wycANVYmbpCyZ7DAAiqopt3IQFIiF/1bYwzWH3/M8RRMKJF8HNyE8KBPDyC/doO5
0c0poCugJI2wOhumLGJShizgSAS85vqijh2Uxpp2yQxdStMnADykzT4Nb5EnEJVE
i7OKy6w+2xyilfFGEWhHfS7uo5Y0zBorhpjgT9fRaqPBGoK4jYwZoO8w97SUd9aS
kNXOCfmFxvcDFSZIYZP77pOeJZR2dLCbr+X9bF1Fz5S4FVkgCXVOp9rmsqzgxoDh
lcpkDh9zPPezdczRkfq/qCf0lmzGg8apdqdr7+g8DxU+01OvPspEdpovQQN0HXlM
m5Kbny4KCWhAgBTyAsOFTEy6lK7u4KsHV1cee3bG+ev65czbQ14gGRMJc/Lhm6Gg
Apcn/UcF14vLWxYVo6lS
=pS6L
-----END PGP SIGNATURE-----
Attachments:
20150727.00-rls-shdep-v2.patchtext/x-diff; name=20150727.00-rls-shdep-v2.patchDownload
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 9096ee5..7781c56 100644
*** a/doc/src/sgml/catalogs.sgml
--- b/doc/src/sgml/catalogs.sgml
***************
*** 5793,5798 ****
--- 5793,5808 ----
</varlistentry>
<varlistentry>
+ <term><symbol>SHARED_DEPENDENCY_POLICY</> (<literal>r</>)</term>
+ <listitem>
+ <para>
+ The referenced object (which must be a role) is mentioned as the
+ target of a dependent policy object.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
<term><symbol>SHARED_DEPENDENCY_PIN</> (<literal>p</>)</term>
<listitem>
<para>
diff --git a/src/backend/catalog/pg_shdepend.c b/src/backend/catalog/pg_shdepend.c
index 34fe4e2..43076c9 100644
*** a/src/backend/catalog/pg_shdepend.c
--- b/src/backend/catalog/pg_shdepend.c
*************** storeObjectDescription(StringInfo descs,
*** 1083,1088 ****
--- 1083,1090 ----
appendStringInfo(descs, _("owner of %s"), objdesc);
else if (deptype == SHARED_DEPENDENCY_ACL)
appendStringInfo(descs, _("privileges for %s"), objdesc);
+ else if (deptype == SHARED_DEPENDENCY_POLICY)
+ appendStringInfo(descs, _("target of %s"), objdesc);
else
elog(ERROR, "unrecognized dependency type: %d",
(int) deptype);
diff --git a/src/backend/commands/policy.c b/src/backend/commands/policy.c
index 17b48d4..9544f75 100644
*** a/src/backend/commands/policy.c
--- b/src/backend/commands/policy.c
***************
*** 22,27 ****
--- 22,28 ----
#include "catalog/indexing.h"
#include "catalog/namespace.h"
#include "catalog/objectaccess.h"
+ #include "catalog/pg_authid.h"
#include "catalog/pg_policy.h"
#include "catalog/pg_type.h"
#include "commands/policy.h"
***************
*** 48,54 ****
static void RangeVarCallbackForPolicy(const RangeVar *rv,
Oid relid, Oid oldrelid, void *arg);
static char parse_policy_command(const char *cmd_name);
! static ArrayType *policy_role_list_to_array(List *roles);
/*
* Callback to RangeVarGetRelidExtended().
--- 49,55 ----
static void RangeVarCallbackForPolicy(const RangeVar *rv,
Oid relid, Oid oldrelid, void *arg);
static char parse_policy_command(const char *cmd_name);
! static Datum *policy_role_list_to_array(List *roles, int *num_roles);
/*
* Callback to RangeVarGetRelidExtended().
*************** parse_policy_command(const char *cmd_nam
*** 130,159 ****
/*
* policy_role_list_to_array
! * helper function to convert a list of RoleSpecs to an array of role ids.
*/
! static ArrayType *
! policy_role_list_to_array(List *roles)
{
! ArrayType *role_ids;
! Datum *temp_array;
ListCell *cell;
- int num_roles;
int i = 0;
/* Handle no roles being passed in as being for public */
if (roles == NIL)
{
! temp_array = (Datum *) palloc(sizeof(Datum));
! temp_array[0] = ObjectIdGetDatum(ACL_ID_PUBLIC);
! role_ids = construct_array(temp_array, 1, OIDOID, sizeof(Oid), true,
! 'i');
! return role_ids;
}
! num_roles = list_length(roles);
! temp_array = (Datum *) palloc(num_roles * sizeof(Datum));
foreach(cell, roles)
{
--- 131,158 ----
/*
* policy_role_list_to_array
! * helper function to convert a list of RoleSpecs to an array of
! * role id Datums.
*/
! static Datum *
! policy_role_list_to_array(List *roles, int *num_roles)
{
! Datum *role_oids;
ListCell *cell;
int i = 0;
/* Handle no roles being passed in as being for public */
if (roles == NIL)
{
! *num_roles = 1;
! role_oids = (Datum *) palloc(*num_roles * sizeof(Datum));
! role_oids[0] = ObjectIdGetDatum(ACL_ID_PUBLIC);
! return role_oids;
}
! *num_roles = list_length(roles);
! role_oids = (Datum *) palloc(*num_roles * sizeof(Datum));
foreach(cell, roles)
{
*************** policy_role_list_to_array(List *roles)
*** 164,187 ****
*/
if (spec->roletype == ROLESPEC_PUBLIC)
{
! if (num_roles != 1)
ereport(WARNING,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("ignoring roles specified other than public"),
errhint("All roles are members of the public role.")));
! temp_array[0] = ObjectIdGetDatum(ACL_ID_PUBLIC);
! num_roles = 1;
! break;
}
else
! temp_array[i++] =
ObjectIdGetDatum(get_rolespec_oid((Node *) spec, false));
}
! role_ids = construct_array(temp_array, num_roles, OIDOID, sizeof(Oid), true,
! 'i');
!
! return role_ids;
}
/*
--- 163,186 ----
*/
if (spec->roletype == ROLESPEC_PUBLIC)
{
! if (*num_roles != 1)
! {
ereport(WARNING,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("ignoring roles specified other than public"),
errhint("All roles are members of the public role.")));
! *num_roles = 1;
! }
! role_oids[0] = ObjectIdGetDatum(ACL_ID_PUBLIC);
!
! return role_oids;
}
else
! role_oids[i++] =
ObjectIdGetDatum(get_rolespec_oid((Node *) spec, false));
}
! return role_oids;
}
/*
*************** CreatePolicy(CreatePolicyStmt *stmt)
*** 463,468 ****
--- 462,469 ----
Relation target_table;
Oid table_id;
char polcmd;
+ Datum *role_oids;
+ int nitems = 0;
ArrayType *role_ids;
ParseState *qual_pstate;
ParseState *with_check_pstate;
*************** CreatePolicy(CreatePolicyStmt *stmt)
*** 476,481 ****
--- 477,483 ----
bool isnull[Natts_pg_policy];
ObjectAddress target;
ObjectAddress myself;
+ int i;
/* Parse command */
polcmd = parse_policy_command(stmt->cmd);
*************** CreatePolicy(CreatePolicyStmt *stmt)
*** 498,506 ****
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("only WITH CHECK expression allowed for INSERT")));
-
/* Collect role ids */
! role_ids = policy_role_list_to_array(stmt->roles);
/* Parse the supplied clause */
qual_pstate = make_parsestate(NULL);
--- 500,509 ----
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("only WITH CHECK expression allowed for INSERT")));
/* Collect role ids */
! role_oids = policy_role_list_to_array(stmt->roles, &nitems);
! role_ids = construct_array(role_oids, nitems, OIDOID,
! sizeof(Oid), true, 'i');
/* Parse the supplied clause */
qual_pstate = make_parsestate(NULL);
*************** CreatePolicy(CreatePolicyStmt *stmt)
*** 614,619 ****
--- 617,634 ----
recordDependencyOnExpr(&myself, with_check_qual,
with_check_pstate->p_rtable, DEPENDENCY_NORMAL);
+ /* Register role dependencies */
+ target.classId = AuthIdRelationId;
+ target.objectSubId = 0;
+ for (i = 0; i < nitems; i++)
+ {
+ target.objectId = DatumGetObjectId(role_oids[i]);
+ /* no dependency if public */
+ if (target.objectId != ACL_ID_PUBLIC)
+ recordSharedDependencyOn(&myself, &target,
+ SHARED_DEPENDENCY_POLICY);
+ }
+
/* Invalidate Relation Cache */
CacheInvalidateRelcache(target_table);
*************** AlterPolicy(AlterPolicyStmt *stmt)
*** 641,646 ****
--- 656,663 ----
Oid policy_id;
Relation target_table;
Oid table_id;
+ Datum *role_oids;
+ int nitems = 0;
ArrayType *role_ids = NULL;
List *qual_parse_rtable = NIL;
List *with_check_parse_rtable = NIL;
*************** AlterPolicy(AlterPolicyStmt *stmt)
*** 658,667 ****
Datum cmd_datum;
char polcmd;
bool polcmd_isnull;
/* Parse role_ids */
if (stmt->roles != NULL)
! role_ids = policy_role_list_to_array(stmt->roles);
/* Get id of table. Also handles permissions checks. */
table_id = RangeVarGetRelidExtended(stmt->table, AccessExclusiveLock,
--- 675,689 ----
Datum cmd_datum;
char polcmd;
bool polcmd_isnull;
+ int i;
/* Parse role_ids */
if (stmt->roles != NULL)
! {
! role_oids = policy_role_list_to_array(stmt->roles, &nitems);
! role_ids = construct_array(role_oids, nitems, OIDOID,
! sizeof(Oid), true, 'i');
! }
/* Get id of table. Also handles permissions checks. */
table_id = RangeVarGetRelidExtended(stmt->table, AccessExclusiveLock,
*************** AlterPolicy(AlterPolicyStmt *stmt)
*** 825,830 ****
--- 847,865 ----
recordDependencyOnExpr(&myself, with_check_qual, with_check_parse_rtable,
DEPENDENCY_NORMAL);
+ /* Register role dependencies */
+ deleteSharedDependencyRecordsFor(PolicyRelationId, policy_id, 0);
+ target.classId = AuthIdRelationId;
+ target.objectSubId = 0;
+ for (i = 0; i < nitems; i++)
+ {
+ target.objectId = DatumGetObjectId(role_oids[i]);
+ /* no dependency if public */
+ if (target.objectId != ACL_ID_PUBLIC)
+ recordSharedDependencyOn(&myself, &target,
+ SHARED_DEPENDENCY_POLICY);
+ }
+
heap_freetuple(new_tuple);
/* Invalidate Relation Cache */
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index aa3f3d9..fbcf904 100644
*** a/src/include/catalog/dependency.h
--- b/src/include/catalog/dependency.h
*************** typedef enum DependencyType
*** 96,101 ****
--- 96,105 ----
* created for the owner of an object; hence two objects may be linked by
* one or the other, but not both, of these dependency types.)
*
+ * (d) a SHARED_DEPENDENCY_POLICY entry means that the referenced object is
+ * a role mentioned in a policy object. The referenced object must be a
+ * pg_authid entry.
+ *
* SHARED_DEPENDENCY_INVALID is a value used as a parameter in internal
* routines, and is not valid in the catalog itself.
*/
*************** typedef enum SharedDependencyType
*** 104,109 ****
--- 108,114 ----
SHARED_DEPENDENCY_PIN = 'p',
SHARED_DEPENDENCY_OWNER = 'o',
SHARED_DEPENDENCY_ACL = 'a',
+ SHARED_DEPENDENCY_POLICY = 'r',
SHARED_DEPENDENCY_INVALID = 0
} SharedDependencyType;
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index e7c242c..0983d5b 100644
*** a/src/test/regress/expected/rowsecurity.out
--- b/src/test/regress/expected/rowsecurity.out
*************** SELECT * FROM coll_t;
*** 2860,2865 ****
--- 2860,2920 ----
ROLLBACK;
--
+ -- Shared Object Dependencies
+ --
+ RESET SESSION AUTHORIZATION;
+ BEGIN;
+ CREATE ROLE alice;
+ CREATE ROLE bob;
+ CREATE TABLE tbl1 (c) AS VALUES ('bar'::text);
+ GRANT SELECT ON TABLE tbl1 TO alice;
+ CREATE POLICY P ON tbl1 TO alice, bob USING (true);
+ SELECT refclassid::regclass, deptype
+ FROM pg_depend
+ WHERE classid = 'pg_policy'::regclass
+ AND refobjid = 'tbl1'::regclass;
+ refclassid | deptype
+ ------------+---------
+ pg_class | a
+ (1 row)
+
+ SELECT refclassid::regclass, deptype
+ FROM pg_shdepend
+ WHERE classid = 'pg_policy'::regclass
+ AND refobjid IN ('alice'::regrole, 'bob'::regrole);
+ refclassid | deptype
+ ------------+---------
+ pg_authid | r
+ pg_authid | r
+ (2 rows)
+
+ SAVEPOINT q;
+ DROP ROLE alice; --fails due to dependency on POLICY p
+ ERROR: role "alice" cannot be dropped because some objects depend on it
+ DETAIL: target of policy p on table tbl1
+ privileges for table tbl1
+ ROLLBACK TO q;
+ ALTER POLICY p ON tbl1 TO bob USING (true);
+ SAVEPOINT q;
+ DROP ROLE alice; --fails due to dependency on GRANT SELECT
+ ERROR: role "alice" cannot be dropped because some objects depend on it
+ DETAIL: privileges for table tbl1
+ ROLLBACK TO q;
+ REVOKE ALL ON TABLE tbl1 FROM alice;
+ SAVEPOINT q;
+ DROP ROLE alice; --succeeds
+ ROLLBACK TO q;
+ SAVEPOINT q;
+ DROP ROLE bob; --fails due to dependency on POLICY p
+ ERROR: role "bob" cannot be dropped because some objects depend on it
+ DETAIL: target of policy p on table tbl1
+ ROLLBACK TO q;
+ DROP POLICY p ON tbl1;
+ SAVEPOINT q;
+ DROP ROLE bob; -- succeeds
+ ROLLBACK TO q;
+ ROLLBACK; -- cleanup
+ --
-- Clean up objects
--
RESET SESSION AUTHORIZATION;
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
index e86f814..a074edd 100644
*** a/src/test/regress/sql/rowsecurity.sql
--- b/src/test/regress/sql/rowsecurity.sql
*************** SELECT * FROM coll_t;
*** 1153,1158 ****
--- 1153,1202 ----
ROLLBACK;
--
+ -- Shared Object Dependencies
+ --
+ RESET SESSION AUTHORIZATION;
+ BEGIN;
+ CREATE ROLE alice;
+ CREATE ROLE bob;
+ CREATE TABLE tbl1 (c) AS VALUES ('bar'::text);
+ GRANT SELECT ON TABLE tbl1 TO alice;
+ CREATE POLICY P ON tbl1 TO alice, bob USING (true);
+ SELECT refclassid::regclass, deptype
+ FROM pg_depend
+ WHERE classid = 'pg_policy'::regclass
+ AND refobjid = 'tbl1'::regclass;
+ SELECT refclassid::regclass, deptype
+ FROM pg_shdepend
+ WHERE classid = 'pg_policy'::regclass
+ AND refobjid IN ('alice'::regrole, 'bob'::regrole);
+
+ SAVEPOINT q;
+ DROP ROLE alice; --fails due to dependency on POLICY p
+ ROLLBACK TO q;
+
+ ALTER POLICY p ON tbl1 TO bob USING (true);
+ SAVEPOINT q;
+ DROP ROLE alice; --fails due to dependency on GRANT SELECT
+ ROLLBACK TO q;
+
+ REVOKE ALL ON TABLE tbl1 FROM alice;
+ SAVEPOINT q;
+ DROP ROLE alice; --succeeds
+ ROLLBACK TO q;
+
+ SAVEPOINT q;
+ DROP ROLE bob; --fails due to dependency on POLICY p
+ ROLLBACK TO q;
+
+ DROP POLICY p ON tbl1;
+ SAVEPOINT q;
+ DROP ROLE bob; -- succeeds
+ ROLLBACK TO q;
+
+ ROLLBACK; -- cleanup
+
+ --
-- Clean up objects
--
RESET SESSION AUTHORIZATION;
On 07/03/2015 10:03 AM, Noah Misch wrote:
+static void +dumpPolicy(Archive *fout, PolicyInfo *polinfo)...
+ if (polinfo->polqual != NULL) + appendPQExpBuffer(query, " USING %s", polinfo->polqual);(3) The USING clause needs parentheses; a dump+reload failed like so:
Also needed for WITH CHECK. Fix pushed to 9.5 and HEAD.
Joe
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 07/03/2015 10:03 AM, Noah Misch wrote:
(4) When DefineQueryRewrite() is about to convert a table to a view, it checks
the table for features unavailable to views. For example, it rejects tables
having triggers. It omits to reject tables having relrowsecurity or a
pg_policy record. Test case:
Please see the attached patch for this issue. Comments?
Thanks,
Joe
Attachments:
20150728.00-rls-tbl2view.patchtext/x-diff; name=20150728.00-rls-tbl2view.patchDownload
diff --git a/src/backend/commands/policy.c b/src/backend/commands/policy.c
index 17b48d4..aa858fa 100644
*** a/src/backend/commands/policy.c
--- b/src/backend/commands/policy.c
*************** get_relation_policy_oid(Oid relid, const
*** 1002,1004 ****
--- 1002,1033 ----
return policy_oid;
}
+
+ /*
+ * relation_has_policies - Determine if relation has any policies
+ */
+ bool
+ relation_has_policies(Relation rel)
+ {
+ Relation catalog;
+ ScanKeyData skey;
+ SysScanDesc sscan;
+ HeapTuple policy_tuple;
+ bool ret = false;
+
+ catalog = heap_open(PolicyRelationId, AccessShareLock);
+ ScanKeyInit(&skey,
+ Anum_pg_policy_polrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(RelationGetRelid(rel)));
+ sscan = systable_beginscan(catalog, PolicyPolrelidPolnameIndexId, true,
+ NULL, 1, &skey);
+ policy_tuple = systable_getnext(sscan);
+ if (HeapTupleIsValid(policy_tuple))
+ ret = true;
+
+ systable_endscan(sscan);
+ heap_close(catalog, AccessShareLock);
+
+ return ret;
+ }
diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c
index a88d73e..39c83a6 100644
*** a/src/backend/rewrite/rewriteDefine.c
--- b/src/backend/rewrite/rewriteDefine.c
***************
*** 27,32 ****
--- 27,33 ----
#include "catalog/objectaccess.h"
#include "catalog/pg_rewrite.h"
#include "catalog/storage.h"
+ #include "commands/policy.h"
#include "miscadmin.h"
#include "nodes/nodeFuncs.h"
#include "parser/parse_utilcmd.h"
*************** DefineQueryRewrite(char *rulename,
*** 410,420 ****
*
* If so, check that the relation is empty because the storage for the
* relation is going to be deleted. Also insist that the rel not have
! * any triggers, indexes, or child tables. (Note: these tests are too
! * strict, because they will reject relations that once had such but
! * don't anymore. But we don't really care, because this whole
! * business of converting relations to views is just a kluge to allow
! * dump/reload of views that participate in circular dependencies.)
*/
if (event_relation->rd_rel->relkind != RELKIND_VIEW &&
event_relation->rd_rel->relkind != RELKIND_MATVIEW)
--- 411,422 ----
*
* If so, check that the relation is empty because the storage for the
* relation is going to be deleted. Also insist that the rel not have
! * any triggers, indexes, child tables, policies, or RLS enabled.
! * (Note: these tests are too strict, because they will reject
! * relations that once had such but don't anymore. But we don't
! * really care, because this whole business of converting relations
! * to views is just a kluge to allow dump/reload of views that
! * participate in circular dependencies.)
*/
if (event_relation->rd_rel->relkind != RELKIND_VIEW &&
event_relation->rd_rel->relkind != RELKIND_MATVIEW)
*************** DefineQueryRewrite(char *rulename,
*** 451,456 ****
--- 453,470 ----
errmsg("could not convert table \"%s\" to a view because it has child tables",
RelationGetRelationName(event_relation))));
+ if (event_relation->rd_rel->relrowsecurity)
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("could not convert table \"%s\" to a view because it has row security enabled",
+ RelationGetRelationName(event_relation))));
+
+ if (relation_has_policies(event_relation))
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("could not convert table \"%s\" to a view because it has row security policies",
+ RelationGetRelationName(event_relation))));
+
RelisBecomingView = true;
}
}
diff --git a/src/include/commands/policy.h b/src/include/commands/policy.h
index ac322e0..be00043 100644
*** a/src/include/commands/policy.h
--- b/src/include/commands/policy.h
*************** extern Oid get_relation_policy_oid(Oid r
*** 31,35 ****
--- 31,36 ----
extern ObjectAddress rename_policy(RenameStmt *stmt);
+ extern bool relation_has_policies(Relation rel);
#endif /* POLICY_H */
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 72361e8..2a8db6d 100644
*** a/src/test/regress/expected/rowsecurity.out
--- b/src/test/regress/expected/rowsecurity.out
*************** SELECT * FROM coll_t;
*** 2908,2913 ****
--- 2908,2936 ----
ROLLBACK;
--
+ -- Converting table to view
+ --
+ BEGIN;
+ SET ROW_SECURITY = FORCE;
+ CREATE TABLE t (c int);
+ CREATE POLICY p ON t USING (c % 2 = 1);
+ ALTER TABLE t ENABLE ROW LEVEL SECURITY;
+ SAVEPOINT q;
+ CREATE RULE "_RETURN" AS ON SELECT TO t DO INSTEAD
+ SELECT * FROM generate_series(1,5) t0(c); -- fails due to row level security enabled
+ ERROR: could not convert table "t" to a view because it has row security enabled
+ ROLLBACK TO q;
+ ALTER TABLE t DISABLE ROW LEVEL SECURITY;
+ SAVEPOINT q;
+ CREATE RULE "_RETURN" AS ON SELECT TO t DO INSTEAD
+ SELECT * FROM generate_series(1,5) t0(c); -- fails due to policy p on t
+ ERROR: could not convert table "t" to a view because it has row security policies
+ ROLLBACK TO q;
+ DROP POLICY p ON t;
+ CREATE RULE "_RETURN" AS ON SELECT TO t DO INSTEAD
+ SELECT * FROM generate_series(1,5) t0(c); -- succeeds
+ ROLLBACK;
+ --
-- Clean up objects
--
RESET SESSION AUTHORIZATION;
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
index f588fa2..ad17ae3 100644
*** a/src/test/regress/sql/rowsecurity.sql
--- b/src/test/regress/sql/rowsecurity.sql
*************** SELECT * FROM coll_t;
*** 1202,1207 ****
--- 1202,1232 ----
ROLLBACK;
--
+ -- Converting table to view
+ --
+ BEGIN;
+ SET ROW_SECURITY = FORCE;
+ CREATE TABLE t (c int);
+ CREATE POLICY p ON t USING (c % 2 = 1);
+ ALTER TABLE t ENABLE ROW LEVEL SECURITY;
+
+ SAVEPOINT q;
+ CREATE RULE "_RETURN" AS ON SELECT TO t DO INSTEAD
+ SELECT * FROM generate_series(1,5) t0(c); -- fails due to row level security enabled
+ ROLLBACK TO q;
+
+ ALTER TABLE t DISABLE ROW LEVEL SECURITY;
+ SAVEPOINT q;
+ CREATE RULE "_RETURN" AS ON SELECT TO t DO INSTEAD
+ SELECT * FROM generate_series(1,5) t0(c); -- fails due to policy p on t
+ ROLLBACK TO q;
+
+ DROP POLICY p ON t;
+ CREATE RULE "_RETURN" AS ON SELECT TO t DO INSTEAD
+ SELECT * FROM generate_series(1,5) t0(c); -- succeeds
+ ROLLBACK;
+
+ --
-- Clean up objects
--
RESET SESSION AUTHORIZATION;
* Joe Conway (joe.conway@crunchydata.com) wrote:
On 07/03/2015 10:03 AM, Noah Misch wrote:
(4) When DefineQueryRewrite() is about to convert a table to a view, it checks
the table for features unavailable to views. For example, it rejects tables
having triggers. It omits to reject tables having relrowsecurity or a
pg_policy record. Test case:Please see the attached patch for this issue. Comments?
Looks good to me.
Thanks!
Stephen
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
On 07/27/2015 05:34 PM, Joe Conway wrote:
On 07/27/2015 01:13 PM, Alvaro Herrera wrote:
Hmm, these are not ACL objects, so conceptually it seems cleaner
to use a different symbol for this. I think the catalog state
and the error messages would be a bit confusing otherwise.Ok -- done
Isn't this leaking the previously allocated array? Not sure
it's all that critical, but still. (I don't think you really
need to call palloc at all here.)Agreed -- I think the attached is a bit cleaner.
Any other comments or concerns?
Pushed to HEAD and 9.5
Joe
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v2.0.22 (GNU/Linux)
iQIcBAEBAgAGBQJVuAp9AAoJEDfy90M199hlokgP/2RVYjPiwM0EcE9pAdGP3Uqa
qkCqj5cPq/5SdLH553h/Xd6056qwchoMJYQo9RZzzVR4wS4HPyeZ00e33RrJZbtm
MGvjat1NJWEyl03y1AwAy9c9yHCRJa5vQlKtm0v2xWDG3xXqwejz/SK2ijw2IWVW
W1/7OnObbquHa7a+IsO1ndXoI0jECzOFDXY6YzFVJuPAf3asOHT44lJbHkfUq3Kv
k5eK14Xrb3kcYqhBQg+eG50lr1aRDj8yDlYQuoGmw/dg/X0TyAo2v+AOaFrN8rXD
igsYaMDafsXY55izkiRuNfcYZAnHC7hNVt+ffV+wLycCTiaEEflp+BCxLTYmh53T
xDB39Lr0Sz7AP77l0M9hbMr3Ao3GA1KFOc9OfRN11eSo5Y6uDtpMeOIFBAke6hxl
DanWPc/YmXzacga99xzOQglDZkDWTohWsEDwniRJmi7UC0Z/gwZ2P60OnwE1lqbd
eOmUu0JZCwklInWoDo9XmWdfp9+OrviGNm0vhQbplhEm/LC9PqBB/DOy44QjSv84
jfY/iMPn8uvGqWiQ/65za1O/1QsRukgp5PVnj7TyNojSskSuAYOF5BcFVIdB7krj
ZKHChreUMVw1nH8py4HkdPOXTHmAItV9/9T2c/UUuJWAECiLy+tIY/if+Tzi+Zn6
nRm99YM401PAsRKLyn0m
=gYmH
-----END PGP SIGNATURE-----
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
On 07/28/2015 11:50 AM, Stephen Frost wrote:
* Joe Conway (joe.conway@crunchydata.com) wrote:
On 07/03/2015 10:03 AM, Noah Misch wrote:
(4) When DefineQueryRewrite() is about to convert a table to a
view, it checks the table for features unavailable to views.
For example, it rejects tables having triggers. It omits to
reject tables having relrowsecurity or a pg_policy record.
Test case:Please see the attached patch for this issue. Comments?
Looks good to me.
Thanks -- pushed to HEAD and 9.5
Joe
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v2.0.22 (GNU/Linux)
iQIcBAEBAgAGBQJVuA+UAAoJEDfy90M199hlrtYQAJGVucRtarWTQoP39of8IYXE
OoAi+q6CKEJWEJdVHmnnCwMc6y0lx0+Qugpb5SAVqMG/0oYvZ/bnvvN5OHMxujCI
bw7A/eJ62BILAt07Oczs1gtt+k4PT1001c4jFjgSC6AV51cZ9UM+d0b4C2YhKcm7
B4tycmC/yGxa5UFT6vdOhneILzBeOMLxgMXKouoD7Gnf4UKh1KUWxxWKU9q0Kl7d
mvRJke1wj/+WfquO09g33+1jHCsnbV4fcQ1q8RQbFPItpqcp4alsuuIYzLiPRbbs
VtZYurFLpjKdJg8OuHw9IYNbDjPZoEuv5eV16aSjOJxnngcXROxwXOwZMVaCuQMq
1D5FoUunJqsqlxVKggJJLfBt9uM/gafjmnRHGBZftfX/A3ZM6c6YewA68Nxo+rTE
xtgA+n1lzWsahje0n2KSFUNRSCqdWcnLQ/HtnVcNjGdFdCxeUDQ5kUAE1hFCMCXe
5eqAvohQQ25RiurZ1rI1IfUoeAPRDp3nvgcMMM7FQMmv6oKNBLr2RivVz0ZE17vd
Htz0y0cPx8mJgEHKMJrV/yF9odECTxevBzkO5rASLCLnGHEYp8WZfqWO/s/HoujS
KU99lfzOfnyBIyl2zIGSmkmCvUIqaP1cUP5xMHIedNhjDRy/Rt6IxwA9qEtgUokI
sC6BWHpxd19RAh5NLXCK
=NFOR
-----END PGP SIGNATURE-----
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 07/03/2015 10:03 AM, Noah Misch wrote:
(6) AlterPolicy() calls InvokeObjectPostAlterHook(PolicyRelationId, ...), but
CreatePolicy() and DropPolicy() lack their respective hook invocations.
Patch attached. Actually AlterPolicy() was also missing its hook -- the
existing InvokeObjectPostAlterHook() was only in rename_policy().
I'm not 100% sure about the hook placement -- would appreciate if
someone could confirm I got it correct.
Joe
Attachments:
20150728.00-rls-hooks.patchtext/x-diff; name=20150728.00-rls-hooks.patchDownload
diff --git a/src/backend/commands/policy.c b/src/backend/commands/policy.c
index 4642d7c..77aaaca 100644
*** a/src/backend/commands/policy.c
--- b/src/backend/commands/policy.c
*************** RemovePolicyById(Oid policy_id)
*** 410,415 ****
--- 410,417 ----
if (!HeapTupleIsValid(tuple))
elog(ERROR, "could not find tuple for policy %u", policy_id);
+ InvokeObjectDropHook(PolicyRelationId, policy_id, 0);
+
/*
* Open and exclusive-lock the relation the policy belong to.
*/
*************** CreatePolicy(CreatePolicyStmt *stmt)
*** 629,634 ****
--- 631,638 ----
SHARED_DEPENDENCY_POLICY);
}
+ InvokeObjectPostCreateHook(PolicyRelationId, policy_id, 0);
+
/* Invalidate Relation Cache */
CacheInvalidateRelcache(target_table);
*************** AlterPolicy(AlterPolicyStmt *stmt)
*** 860,865 ****
--- 864,871 ----
SHARED_DEPENDENCY_POLICY);
}
+ InvokeObjectPostAlterHook(PolicyRelationId, policy_id, 0);
+
heap_freetuple(new_tuple);
/* Invalidate Relation Cache */
On 07/03/2015 10:03 AM, Noah Misch wrote:
(7) Using an aggregate function in a policy predicate elicits an inapposite
error message due to use of EXPR_KIND_WHERE for parse analysis. Need a new
ParseExprKind. Test case:
Patch attached. Comments?
Joe
Attachments:
20150728.00-rls-expr_kind.patchtext/x-diff; name=20150728.00-rls-expr_kind.patchDownload
diff --git a/src/backend/commands/policy.c b/src/backend/commands/policy.c
index 4642d7c..d8ef496 100644
*** a/src/backend/commands/policy.c
--- b/src/backend/commands/policy.c
*************** CreatePolicy(CreatePolicyStmt *stmt)
*** 532,545 ****
NULL, false, false);
addRTEtoQuery(with_check_pstate, rte, false, true, true);
! qual = transformWhereClause(qual_pstate,
copyObject(stmt->qual),
! EXPR_KIND_WHERE,
"POLICY");
! with_check_qual = transformWhereClause(with_check_pstate,
copyObject(stmt->with_check),
! EXPR_KIND_WHERE,
"POLICY");
/* Fix up collation information */
--- 532,545 ----
NULL, false, false);
addRTEtoQuery(with_check_pstate, rte, false, true, true);
! qual = transformPolicyClause(qual_pstate,
copyObject(stmt->qual),
! EXPR_KIND_POLICY,
"POLICY");
! with_check_qual = transformPolicyClause(with_check_pstate,
copyObject(stmt->with_check),
! EXPR_KIND_POLICY,
"POLICY");
/* Fix up collation information */
*************** AlterPolicy(AlterPolicyStmt *stmt)
*** 704,711 ****
addRTEtoQuery(qual_pstate, rte, false, true, true);
! qual = transformWhereClause(qual_pstate, copyObject(stmt->qual),
! EXPR_KIND_WHERE,
"POLICY");
/* Fix up collation information */
--- 704,711 ----
addRTEtoQuery(qual_pstate, rte, false, true, true);
! qual = transformPolicyClause(qual_pstate, copyObject(stmt->qual),
! EXPR_KIND_POLICY,
"POLICY");
/* Fix up collation information */
*************** AlterPolicy(AlterPolicyStmt *stmt)
*** 726,734 ****
addRTEtoQuery(with_check_pstate, rte, false, true, true);
! with_check_qual = transformWhereClause(with_check_pstate,
copyObject(stmt->with_check),
! EXPR_KIND_WHERE,
"POLICY");
/* Fix up collation information */
--- 726,734 ----
addRTEtoQuery(with_check_pstate, rte, false, true, true);
! with_check_qual = transformPolicyClause(with_check_pstate,
copyObject(stmt->with_check),
! EXPR_KIND_POLICY,
"POLICY");
/* Fix up collation information */
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
index 478d8ca..b79e73d 100644
*** a/src/backend/parser/parse_agg.c
--- b/src/backend/parser/parse_agg.c
*************** check_agglevels_and_constraints(ParseSta
*** 373,378 ****
--- 373,381 ----
case EXPR_KIND_WHERE:
errkind = true;
break;
+ case EXPR_KIND_POLICY:
+ errkind = true;
+ break;
case EXPR_KIND_HAVING:
/* okay */
break;
*************** transformWindowFuncCall(ParseState *psta
*** 770,775 ****
--- 773,781 ----
case EXPR_KIND_WHERE:
errkind = true;
break;
+ case EXPR_KIND_POLICY:
+ errkind = true;
+ break;
case EXPR_KIND_HAVING:
errkind = true;
break;
diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c
index 5980856..7063e5f 100644
*** a/src/backend/parser/parse_clause.c
--- b/src/backend/parser/parse_clause.c
*************** transformWhereClause(ParseState *pstate,
*** 1465,1470 ****
--- 1465,1491 ----
return qual;
}
+ /*
+ * transformPolicyClause -
+ * Transform USING and WITH CHECK expr to make sure it is of type boolean.
+ *
+ * constructName does not affect the semantics, but is used in error messages
+ */
+ Node *
+ transformPolicyClause(ParseState *pstate, Node *clause,
+ ParseExprKind exprKind, const char *constructName)
+ {
+ Node *qual;
+
+ if (clause == NULL)
+ return NULL;
+
+ qual = transformExpr(pstate, clause, exprKind);
+
+ qual = coerce_to_boolean(pstate, qual, constructName);
+
+ return qual;
+ }
/*
* transformLimitClause -
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 0ff46dd..fa77ef1 100644
*** a/src/backend/parser/parse_expr.c
--- b/src/backend/parser/parse_expr.c
*************** transformSubLink(ParseState *pstate, Sub
*** 1672,1677 ****
--- 1672,1678 ----
case EXPR_KIND_FROM_SUBSELECT:
case EXPR_KIND_FROM_FUNCTION:
case EXPR_KIND_WHERE:
+ case EXPR_KIND_POLICY:
case EXPR_KIND_HAVING:
case EXPR_KIND_FILTER:
case EXPR_KIND_WINDOW_PARTITION:
*************** ParseExprKindName(ParseExprKind exprKind
*** 3173,3178 ****
--- 3174,3181 ----
return "function in FROM";
case EXPR_KIND_WHERE:
return "WHERE";
+ case EXPR_KIND_POLICY:
+ return "POLICY";
case EXPR_KIND_HAVING:
return "HAVING";
case EXPR_KIND_FILTER:
diff --git a/src/include/parser/parse_clause.h b/src/include/parser/parse_clause.h
index 77619e3..63e83e1 100644
*** a/src/include/parser/parse_clause.h
--- b/src/include/parser/parse_clause.h
*************** extern bool interpretOidsOption(List *de
*** 24,29 ****
--- 24,31 ----
extern Node *transformWhereClause(ParseState *pstate, Node *clause,
ParseExprKind exprKind, const char *constructName);
+ extern Node *transformPolicyClause(ParseState *pstate, Node *clause,
+ ParseExprKind exprKind, const char *constructName);
extern Node *transformLimitClause(ParseState *pstate, Node *clause,
ParseExprKind exprKind, const char *constructName);
extern List *transformGroupClause(ParseState *pstate, List *grouplist,
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index 7ecaffc..5249945 100644
*** a/src/include/parser/parse_node.h
--- b/src/include/parser/parse_node.h
*************** typedef enum ParseExprKind
*** 63,69 ****
EXPR_KIND_INDEX_PREDICATE, /* index predicate */
EXPR_KIND_ALTER_COL_TRANSFORM, /* transform expr in ALTER COLUMN TYPE */
EXPR_KIND_EXECUTE_PARAMETER, /* parameter value in EXECUTE */
! EXPR_KIND_TRIGGER_WHEN /* WHEN condition in CREATE TRIGGER */
} ParseExprKind;
--- 63,70 ----
EXPR_KIND_INDEX_PREDICATE, /* index predicate */
EXPR_KIND_ALTER_COL_TRANSFORM, /* transform expr in ALTER COLUMN TYPE */
EXPR_KIND_EXECUTE_PARAMETER, /* parameter value in EXECUTE */
! EXPR_KIND_TRIGGER_WHEN, /* WHEN condition in CREATE TRIGGER */
! EXPR_KIND_POLICY /* USING or WITH CHECK expr in policy */
} ParseExprKind;
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index b146da3..23e9a55 100644
*** a/src/test/regress/expected/rowsecurity.out
--- b/src/test/regress/expected/rowsecurity.out
*************** CREATE RULE "_RETURN" AS ON SELECT TO t
*** 3024,3029 ****
--- 3024,3038 ----
SELECT * FROM generate_series(1,5) t0(c); -- succeeds
ROLLBACK;
--
+ -- Policy expression handling
+ --
+ BEGIN;
+ SET row_security = FORCE;
+ CREATE TABLE t (c) AS VALUES ('bar'::text);
+ CREATE POLICY p ON t USING (max(c)); -- fails: aggregate functions are not allowed in POLICY
+ ERROR: aggregate functions are not allowed in POLICY
+ ROLLBACK;
+ --
-- Clean up objects
--
RESET SESSION AUTHORIZATION;
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
index 54f2c89..47a2703 100644
*** a/src/test/regress/sql/rowsecurity.sql
--- b/src/test/regress/sql/rowsecurity.sql
*************** CREATE RULE "_RETURN" AS ON SELECT TO t
*** 1290,1295 ****
--- 1290,1304 ----
ROLLBACK;
--
+ -- Policy expression handling
+ --
+ BEGIN;
+ SET row_security = FORCE;
+ CREATE TABLE t (c) AS VALUES ('bar'::text);
+ CREATE POLICY p ON t USING (max(c)); -- fails: aggregate functions are not allowed in POLICY
+ ROLLBACK;
+
+ --
-- Clean up objects
--
RESET SESSION AUTHORIZATION;
On 29 July 2015 at 02:36, Joe Conway <joe.conway@crunchydata.com> wrote:
On 07/03/2015 10:03 AM, Noah Misch wrote:
(6) AlterPolicy() calls InvokeObjectPostAlterHook(PolicyRelationId, ...), but
CreatePolicy() and DropPolicy() lack their respective hook invocations.Patch attached. Actually AlterPolicy() was also missing its hook -- the
existing InvokeObjectPostAlterHook() was only in rename_policy().I'm not 100% sure about the hook placement -- would appreciate if
someone could confirm I got it correct.
The CreatePolicy() and AlterPolicy() changes look OK to me, but the
RemovePolicyById() change looks to be unnecessary ---
RemovePolicyById() is called only from doDeletion(), which in turned
is called only from deleteOneObject(), which already invokes the drop
hooks. So ISTM that RemovePolicyById() doesn't need to do anything,
right?
Regards,
Dean
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 29 July 2015 at 05:02, Joe Conway <joe.conway@crunchydata.com> wrote:
On 07/03/2015 10:03 AM, Noah Misch wrote:
(7) Using an aggregate function in a policy predicate elicits an inapposite
error message due to use of EXPR_KIND_WHERE for parse analysis. Need a new
ParseExprKind. Test case:Patch attached. Comments?
I don't think there is any point in adding the new function
transformPolicyClause(), which is identical to transformWhereClause().
You can just use transformWhereClause() with EXPR_KIND_POLICY. It's
already used for lots of other expression kinds.
There are a couple of places in test_rls_hooks.c that also need updating.
I think that check_agglevels_and_constraints() and
transformWindowFuncCall() could be made to emit more targeted error
messages for EXPR_KIND_POLICY, for example "aggregate functions are
not allowed in policy USING and WITH CHECK expressions".
Regards,
Dean
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 07/29/2015 01:01 AM, Dean Rasheed wrote:
The CreatePolicy() and AlterPolicy() changes look OK to me, but the
RemovePolicyById() change looks to be unnecessary ---
RemovePolicyById() is called only from doDeletion(), which in turned
is called only from deleteOneObject(), which already invokes the drop
hooks. So ISTM that RemovePolicyById() doesn't need to do anything,
right?
Seems correct. Will remove that change and commit it that way.
Joe
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 07/29/2015 02:41 AM, Dean Rasheed wrote:
I don't think there is any point in adding the new function
transformPolicyClause(), which is identical to transformWhereClause().
You can just use transformWhereClause() with EXPR_KIND_POLICY. It's
already used for lots of other expression kinds.
Yeah, I was debating that. Although I do think the name
transformWhereClause() is unfortunate. Maybe it ought to be renamed
transformBooleanExpr() or something similar?
There are a couple of places in test_rls_hooks.c that also need updating.
Right -- thanks
I think that check_agglevels_and_constraints() and
transformWindowFuncCall() could be made to emit more targeted error
messages for EXPR_KIND_POLICY, for example "aggregate functions are
not allowed in policy USING and WITH CHECK expressions".
ok
Thanks for the review.
Joe
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 29 July 2015 at 16:52, Joe Conway <joe.conway@crunchydata.com> wrote:
On 07/29/2015 02:41 AM, Dean Rasheed wrote:
I don't think there is any point in adding the new function
transformPolicyClause(), which is identical to transformWhereClause().
You can just use transformWhereClause() with EXPR_KIND_POLICY. It's
already used for lots of other expression kinds.Yeah, I was debating that. Although I do think the name
transformWhereClause() is unfortunate. Maybe it ought to be renamed
transformBooleanExpr() or something similar?
I expect it's probably being used by other people's code though, so
renaming it might cause pain for quite a few people.
Regards,
Dean
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 07/29/2015 08:46 AM, Joe Conway wrote:
On 07/29/2015 01:01 AM, Dean Rasheed wrote:
The CreatePolicy() and AlterPolicy() changes look OK to me, but the
RemovePolicyById() change looks to be unnecessary ---
RemovePolicyById() is called only from doDeletion(), which in turned
is called only from deleteOneObject(), which already invokes the drop
hooks. So ISTM that RemovePolicyById() doesn't need to do anything,
right?Seems correct. Will remove that change and commit it that way.
Pushed to HEAD and 9.5.
Joe
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 07/29/2015 02:41 AM, Dean Rasheed wrote:
I don't think there is any point in adding the new function
transformPolicyClause(), which is identical to transformWhereClause().
You can just use transformWhereClause() with EXPR_KIND_POLICY. It's
already used for lots of other expression kinds.
Ok -- I went back to using transformWhereClause. I'd still prefer to
change the name -- more than half the uses of the function are for other
than EXPR_KIND_WHERE -- but I don't feel that strongly about it.
There are a couple of places in test_rls_hooks.c that also need updating.
done
I think that check_agglevels_and_constraints() and
transformWindowFuncCall() could be made to emit more targeted error
messages for EXPR_KIND_POLICY, for example "aggregate functions are
not allowed in policy USING and WITH CHECK expressions".
done
Unless there are other comments/objections, will commit this later today.
Joe
Attachments:
20150729.00-rls-expr_kind-v2.patchtext/x-diff; name=20150729.00-rls-expr_kind-v2.patchDownload
diff --git a/src/backend/commands/policy.c b/src/backend/commands/policy.c
index d8b4390..bcf4a8f 100644
*** a/src/backend/commands/policy.c
--- b/src/backend/commands/policy.c
*************** CreatePolicy(CreatePolicyStmt *stmt)
*** 534,545 ****
qual = transformWhereClause(qual_pstate,
copyObject(stmt->qual),
! EXPR_KIND_WHERE,
"POLICY");
with_check_qual = transformWhereClause(with_check_pstate,
copyObject(stmt->with_check),
! EXPR_KIND_WHERE,
"POLICY");
/* Fix up collation information */
--- 534,545 ----
qual = transformWhereClause(qual_pstate,
copyObject(stmt->qual),
! EXPR_KIND_POLICY,
"POLICY");
with_check_qual = transformWhereClause(with_check_pstate,
copyObject(stmt->with_check),
! EXPR_KIND_POLICY,
"POLICY");
/* Fix up collation information */
*************** AlterPolicy(AlterPolicyStmt *stmt)
*** 707,713 ****
addRTEtoQuery(qual_pstate, rte, false, true, true);
qual = transformWhereClause(qual_pstate, copyObject(stmt->qual),
! EXPR_KIND_WHERE,
"POLICY");
/* Fix up collation information */
--- 707,713 ----
addRTEtoQuery(qual_pstate, rte, false, true, true);
qual = transformWhereClause(qual_pstate, copyObject(stmt->qual),
! EXPR_KIND_POLICY,
"POLICY");
/* Fix up collation information */
*************** AlterPolicy(AlterPolicyStmt *stmt)
*** 730,736 ****
with_check_qual = transformWhereClause(with_check_pstate,
copyObject(stmt->with_check),
! EXPR_KIND_WHERE,
"POLICY");
/* Fix up collation information */
--- 730,736 ----
with_check_qual = transformWhereClause(with_check_pstate,
copyObject(stmt->with_check),
! EXPR_KIND_POLICY,
"POLICY");
/* Fix up collation information */
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
index 478d8ca..e33adf5 100644
*** a/src/backend/parser/parse_agg.c
--- b/src/backend/parser/parse_agg.c
*************** check_agglevels_and_constraints(ParseSta
*** 373,378 ****
--- 373,385 ----
case EXPR_KIND_WHERE:
errkind = true;
break;
+ case EXPR_KIND_POLICY:
+ if (isAgg)
+ err = _("aggregate functions are not allowed in POLICY USING and WITH CHECK expressions");
+ else
+ err = _("grouping operations are not allowed in POLICY USING and WITH CHECK expressions");
+
+ break;
case EXPR_KIND_HAVING:
/* okay */
break;
*************** transformWindowFuncCall(ParseState *psta
*** 770,775 ****
--- 777,785 ----
case EXPR_KIND_WHERE:
errkind = true;
break;
+ case EXPR_KIND_POLICY:
+ err = _("aggregate functions are not allowed in POLICY USING and WITH CHECK expressions");
+ break;
case EXPR_KIND_HAVING:
errkind = true;
break;
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 0ff46dd..fa77ef1 100644
*** a/src/backend/parser/parse_expr.c
--- b/src/backend/parser/parse_expr.c
*************** transformSubLink(ParseState *pstate, Sub
*** 1672,1677 ****
--- 1672,1678 ----
case EXPR_KIND_FROM_SUBSELECT:
case EXPR_KIND_FROM_FUNCTION:
case EXPR_KIND_WHERE:
+ case EXPR_KIND_POLICY:
case EXPR_KIND_HAVING:
case EXPR_KIND_FILTER:
case EXPR_KIND_WINDOW_PARTITION:
*************** ParseExprKindName(ParseExprKind exprKind
*** 3173,3178 ****
--- 3174,3181 ----
return "function in FROM";
case EXPR_KIND_WHERE:
return "WHERE";
+ case EXPR_KIND_POLICY:
+ return "POLICY";
case EXPR_KIND_HAVING:
return "HAVING";
case EXPR_KIND_FILTER:
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index 7ecaffc..5249945 100644
*** a/src/include/parser/parse_node.h
--- b/src/include/parser/parse_node.h
*************** typedef enum ParseExprKind
*** 63,69 ****
EXPR_KIND_INDEX_PREDICATE, /* index predicate */
EXPR_KIND_ALTER_COL_TRANSFORM, /* transform expr in ALTER COLUMN TYPE */
EXPR_KIND_EXECUTE_PARAMETER, /* parameter value in EXECUTE */
! EXPR_KIND_TRIGGER_WHEN /* WHEN condition in CREATE TRIGGER */
} ParseExprKind;
--- 63,70 ----
EXPR_KIND_INDEX_PREDICATE, /* index predicate */
EXPR_KIND_ALTER_COL_TRANSFORM, /* transform expr in ALTER COLUMN TYPE */
EXPR_KIND_EXECUTE_PARAMETER, /* parameter value in EXECUTE */
! EXPR_KIND_TRIGGER_WHEN, /* WHEN condition in CREATE TRIGGER */
! EXPR_KIND_POLICY /* USING or WITH CHECK expr in policy */
} ParseExprKind;
diff --git a/src/test/modules/test_rls_hooks/test_rls_hooks.c b/src/test/modules/test_rls_hooks/test_rls_hooks.c
index 61b62d5..d76b17a 100644
*** a/src/test/modules/test_rls_hooks/test_rls_hooks.c
--- b/src/test/modules/test_rls_hooks/test_rls_hooks.c
*************** test_rls_hooks_permissive(CmdType cmdtyp
*** 106,112 ****
e = (Node *) makeSimpleA_Expr(AEXPR_OP, "=", (Node *) n, (Node *) c, 0);
policy->qual = (Expr *) transformWhereClause(qual_pstate, copyObject(e),
! EXPR_KIND_WHERE,
"POLICY");
policy->with_check_qual = copyObject(policy->qual);
--- 106,112 ----
e = (Node *) makeSimpleA_Expr(AEXPR_OP, "=", (Node *) n, (Node *) c, 0);
policy->qual = (Expr *) transformWhereClause(qual_pstate, copyObject(e),
! EXPR_KIND_POLICY,
"POLICY");
policy->with_check_qual = copyObject(policy->qual);
*************** test_rls_hooks_restrictive(CmdType cmdty
*** 160,166 ****
e = (Node *) makeSimpleA_Expr(AEXPR_OP, "=", (Node *) n, (Node *) c, 0);
policy->qual = (Expr *) transformWhereClause(qual_pstate, copyObject(e),
! EXPR_KIND_WHERE,
"POLICY");
policy->with_check_qual = copyObject(policy->qual);
--- 160,166 ----
e = (Node *) makeSimpleA_Expr(AEXPR_OP, "=", (Node *) n, (Node *) c, 0);
policy->qual = (Expr *) transformWhereClause(qual_pstate, copyObject(e),
! EXPR_KIND_POLICY,
"POLICY");
policy->with_check_qual = copyObject(policy->qual);
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index b146da3..22e89bc 100644
*** a/src/test/regress/expected/rowsecurity.out
--- b/src/test/regress/expected/rowsecurity.out
*************** CREATE RULE "_RETURN" AS ON SELECT TO t
*** 3024,3029 ****
--- 3024,3038 ----
SELECT * FROM generate_series(1,5) t0(c); -- succeeds
ROLLBACK;
--
+ -- Policy expression handling
+ --
+ BEGIN;
+ SET row_security = FORCE;
+ CREATE TABLE t (c) AS VALUES ('bar'::text);
+ CREATE POLICY p ON t USING (max(c)); -- fails: aggregate functions are not allowed in POLICY
+ ERROR: aggregate functions are not allowed in POLICY USING and WITH CHECK expressions
+ ROLLBACK;
+ --
-- Clean up objects
--
RESET SESSION AUTHORIZATION;
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
index 54f2c89..47a2703 100644
*** a/src/test/regress/sql/rowsecurity.sql
--- b/src/test/regress/sql/rowsecurity.sql
*************** CREATE RULE "_RETURN" AS ON SELECT TO t
*** 1290,1295 ****
--- 1290,1304 ----
ROLLBACK;
--
+ -- Policy expression handling
+ --
+ BEGIN;
+ SET row_security = FORCE;
+ CREATE TABLE t (c) AS VALUES ('bar'::text);
+ CREATE POLICY p ON t USING (max(c)); -- fails: aggregate functions are not allowed in POLICY
+ ROLLBACK;
+
+ --
-- Clean up objects
--
RESET SESSION AUTHORIZATION;
On 29 July 2015 at 20:36, Joe Conway <joe.conway@crunchydata.com> wrote:
On 07/29/2015 02:41 AM, Dean Rasheed wrote:
I don't think there is any point in adding the new function
transformPolicyClause(), which is identical to transformWhereClause().
You can just use transformWhereClause() with EXPR_KIND_POLICY. It's
already used for lots of other expression kinds.Ok -- I went back to using transformWhereClause. I'd still prefer to
change the name -- more than half the uses of the function are for other
than EXPR_KIND_WHERE -- but I don't feel that strongly about it.There are a couple of places in test_rls_hooks.c that also need updating.
done
I think that check_agglevels_and_constraints() and
transformWindowFuncCall() could be made to emit more targeted error
messages for EXPR_KIND_POLICY, for example "aggregate functions are
not allowed in policy USING and WITH CHECK expressions".done
Unless there are other comments/objections, will commit this later today.
In the final error s/aggregate/window/. Other than that it looks good to me.
Regards,
Dean
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Joe Conway wrote:
On 07/29/2015 02:41 AM, Dean Rasheed wrote:
I don't think there is any point in adding the new function
transformPolicyClause(), which is identical to transformWhereClause().
You can just use transformWhereClause() with EXPR_KIND_POLICY. It's
already used for lots of other expression kinds.Ok -- I went back to using transformWhereClause. I'd still prefer to
change the name -- more than half the uses of the function are for other
than EXPR_KIND_WHERE -- but I don't feel that strongly about it.
Currently the comment about it says "for WHERE and allied", maybe this
should be a bit more explicit. I think it was originally for things
like WHERE and HAVING, and usage slowly extended beyond that. Does it
make sense to consider the USING clause in CREATE/ALTER POLICY as an
"allied" clause of WHERE? I don't particularly think so. I'm just
talking about a small rewording of the comment.
I think that check_agglevels_and_constraints() and
transformWindowFuncCall() could be made to emit more targeted error
messages for EXPR_KIND_POLICY, for example "aggregate functions are
not allowed in policy USING and WITH CHECK expressions".done
+ case EXPR_KIND_POLICY: + if (isAgg) + err = _("aggregate functions are not allowed in POLICY USING and WITH CHECK expressions"); + else + err = _("grouping operations are not allowed in POLICY USING and WITH CHECK expressions");
I think this reads a bit funny. What's a "POLICY USING" clause? I
expect that translators will treat the two words POLICY USING as a
single token, and the result is not going to make any sense.
Maybe "in a policy's USING and WITH CHECK expressions", or perhaps "in
policies's USING and WITH CHECK exprs", not sure.
--
�lvaro Herrera http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Wed, Jul 29, 2015 at 3:58 PM, Alvaro Herrera
<alvherre@2ndquadrant.com> wrote:
I think this reads a bit funny. What's a "POLICY USING" clause? I
expect that translators will treat the two words POLICY USING as a
single token, and the result is not going to make any sense.Maybe "in a policy's USING and WITH CHECK expressions", or perhaps "in
policies's USING and WITH CHECK exprs", not sure.
Yeah, I don't see why we would capitalize POLICY there.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 07/29/2015 01:26 PM, Robert Haas wrote:
On Wed, Jul 29, 2015 at 3:58 PM, Alvaro Herrera
<alvherre@2ndquadrant.com> wrote:I think this reads a bit funny. What's a "POLICY USING" clause? I
expect that translators will treat the two words POLICY USING as a
single token, and the result is not going to make any sense.Maybe "in a policy's USING and WITH CHECK expressions", or perhaps "in
policies's USING and WITH CHECK exprs", not sure.Yeah, I don't see why we would capitalize POLICY there.
The equivalent message for functions is:
".. are not allowed in functions in FROM"
So how does this sound:
"... are not allowed in policies in USING and WITH CHECK expressions"
or perhaps more simply:
"... are not allowed in policies in USING and WITH CHECK"
Joe
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Wed, Jul 29, 2015 at 4:57 PM, Joe Conway <joe.conway@crunchydata.com> wrote:
On 07/29/2015 01:26 PM, Robert Haas wrote:
On Wed, Jul 29, 2015 at 3:58 PM, Alvaro Herrera
<alvherre@2ndquadrant.com> wrote:I think this reads a bit funny. What's a "POLICY USING" clause? I
expect that translators will treat the two words POLICY USING as a
single token, and the result is not going to make any sense.Maybe "in a policy's USING and WITH CHECK expressions", or perhaps "in
policies's USING and WITH CHECK exprs", not sure.Yeah, I don't see why we would capitalize POLICY there.
The equivalent message for functions is:
".. are not allowed in functions in FROM"So how does this sound:
"... are not allowed in policies in USING and WITH CHECK expressions"
or perhaps more simply:
"... are not allowed in policies in USING and WITH CHECK"
Awkward. The "in policies in" phrasing is just hard to read. Why not
just "in policy expressions"? There's no third kind that does allow
these.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Robert Haas wrote:
On Wed, Jul 29, 2015 at 4:57 PM, Joe Conway <joe.conway@crunchydata.com> wrote:
The equivalent message for functions is:
".. are not allowed in functions in FROM"So how does this sound:
"... are not allowed in policies in USING and WITH CHECK expressions"
or perhaps more simply:
"... are not allowed in policies in USING and WITH CHECK"Awkward. The "in policies in" phrasing is just hard to read.
Yeah. Besides, it's not really the same thing.
Why not just "in policy expressions"? There's no third kind that does
allow these.
WFM
--
�lvaro Herrera http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 07/29/2015 02:04 PM, Alvaro Herrera wrote:
Why not just "in policy expressions"? There's no third kind that does
allow these.WFM
Sold! Will do it that way.
Joe
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 07/29/2015 02:56 PM, Joe Conway wrote:
On 07/29/2015 02:04 PM, Alvaro Herrera wrote:
Why not just "in policy expressions"? There's no third kind that does
allow these.WFM
Sold! Will do it that way.
Committed/pushed to HEAD and 9.5.
Joe
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Noah,
First off, thanks again for your review and comments on RLS. Hopefully
this addresses the last remaining open item from that review.
* Noah Misch (noah@leadboat.com) wrote:
On Wed, Feb 25, 2015 at 11:37:24PM -0500, Stephen Frost wrote:
+ <para> + Referential integrity checks, such as unique or primary key constraints + and foreign key references, will bypass row security to ensure that + data integrity is maintained. Care must be taken when developing + schemas and row level policies to avoid a "covert channel" leak of + information through these referential integrity checks....
+ /* + * Row-level security should be disabled in the case where a foreign-key + * relation is queried to check existence of tuples that references the + * primary-key being modified. + */ + temp_sec_context = save_sec_context | SECURITY_LOCAL_USERID_CHANGE; + if (qkey->constr_queryno == RI_PLAN_CHECK_LOOKUPPK + || qkey->constr_queryno == RI_PLAN_CHECK_LOOKUPPK_FROM_PK + || qkey->constr_queryno == RI_PLAN_RESTRICT_DEL_CHECKREF + || qkey->constr_queryno == RI_PLAN_RESTRICT_UPD_CHECKREF) + temp_sec_context |= SECURITY_ROW_LEVEL_DISABLED;(5) This code does not use SECURITY_ROW_LEVEL_DISABLED for foreign keys with
CASCADE, SET NULL or SET DEFAULT actions. The associated documentation says
nothing about this presumably-important distinction.
I believe the original intent was to only include the queries which were
against the primary table, but reviewing what ends up happening, that
clearly doesn't actually make sense. As you note below, this is only
to address the 'row_security = force' mode, which I suspect is why it
wasn't picked up in earlier testing.
Is SECURITY_ROW_LEVEL_DISABLED mode even needed? We switch users to the owner
of the FROM-clause table before running an RI query. That means use of this
mode can only matter under row_security=force, right? I would rest easier if
this mode went away, because it is a security landmine. If user code managed
to run in this mode, it would bypass every policy in the database. (I find no
such vulnerability today, because we use the mode only for parse analysis of
ri_triggers.c queries.)
You're right, the reason for it to exist was to address the
'row_security = force' case. I spent a bit of time considering that and
have come up with the attached for consideration (which needs additional
work before being committed, but should get the idea across clearly).
This removes the notion of SECURITY_ROW_LEVEL_DISABLED entirely and
instead ignores 'row_security = force' if InLocalUserIdChange() is true.
Further, this changes row_security to have GUC_NOT_WHILE_SEC_REST set.
I didn't set GUC_NO_RESET_ALL for it though as the default is for
row_security to be 'on'.
The biggest drawback that I can see to this is that users won't be able
to set the row_security GUC inside of a security definer function. I
can imagine use-cases where someone might want to change it in a
security definer function but it doesn't seem like they're valuable
enough to counter your argument (which I agree with) that
SECURITY_ROW_LEVEL_DISABLED is a security landmine.
Another approach which I considered was to add a new 'RLS_IGNORE_FORCE'
flag, which would, again, ignore 'row_security=force' when set (meaning
owners would bypass RLS regardless of the actual row_security setting).
I didn't like adding that to sec_context though and it wasn't clear
where a good place would be. Further, it seems like it'd be nice to
have a generic flag that says "we're running an internal referential
integrity operation, don't get in the way", though that could also be a
risky flag to have. Such an approach would allow us to differentiate RI
queries from operations run under a security definer function though.
Thoughts?
Thanks!
Stephen
Attachments:
remove-rls-disabled.v1.patchtext/x-diff; charset=us-asciiDownload
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
new file mode 100644
index 61edde9..647ea77
*** a/src/backend/utils/adt/ri_triggers.c
--- b/src/backend/utils/adt/ri_triggers.c
*************** ri_PlanCheck(const char *querystr, int n
*** 2984,3001 ****
/* Switch to proper UID to perform check as */
GetUserIdAndSecContext(&save_userid, &save_sec_context);
- /*
- * Row-level security should be disabled in the case where a foreign-key
- * relation is queried to check existence of tuples that references the
- * primary-key being modified.
- */
temp_sec_context = save_sec_context | SECURITY_LOCAL_USERID_CHANGE;
- if (qkey->constr_queryno == RI_PLAN_CHECK_LOOKUPPK
- || qkey->constr_queryno == RI_PLAN_CHECK_LOOKUPPK_FROM_PK
- || qkey->constr_queryno == RI_PLAN_RESTRICT_DEL_CHECKREF
- || qkey->constr_queryno == RI_PLAN_RESTRICT_UPD_CHECKREF)
- temp_sec_context |= SECURITY_ROW_LEVEL_DISABLED;
-
SetUserIdAndSecContext(RelationGetForm(query_rel)->relowner,
temp_sec_context);
--- 2984,2990 ----
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
new file mode 100644
index 525794f..d0bcff9
*** a/src/backend/utils/cache/plancache.c
--- b/src/backend/utils/cache/plancache.c
*************** CreateCachedPlan(Node *raw_parse_tree,
*** 204,211 ****
plansource->total_custom_cost = 0;
plansource->num_custom_plans = 0;
plansource->hasRowSecurity = false;
! plansource->rowSecurityDisabled = InRowLevelSecurityDisabled();
! plansource->row_security_env = row_security;
plansource->planUserId = InvalidOid;
MemoryContextSwitchTo(oldcxt);
--- 204,212 ----
plansource->total_custom_cost = 0;
plansource->num_custom_plans = 0;
plansource->hasRowSecurity = false;
! plansource->row_security_env = InLocalUserIdChange() &&
! row_security == ROW_SECURITY_FORCE ?
! ROW_SECURITY_ON : row_security;
plansource->planUserId = InvalidOid;
MemoryContextSwitchTo(oldcxt);
*************** RevalidateCachedQuery(CachedPlanSource *
*** 580,586 ****
if (!OidIsValid(plansource->planUserId))
{
plansource->planUserId = GetUserId();
! plansource->row_security_env = row_security;
}
/*
--- 581,588 ----
if (!OidIsValid(plansource->planUserId))
{
plansource->planUserId = GetUserId();
! plansource->row_security_env = InLocalUserIdChange() ? ROW_SECURITY_ON :
! row_security;
}
/*
*************** RevalidateCachedQuery(CachedPlanSource *
*** 611,617 ****
* setting has been changed.
*/
if (plansource->is_valid
- && !plansource->rowSecurityDisabled
&& plansource->hasRowSecurity
&& (plansource->planUserId != GetUserId()
|| plansource->row_security_env != row_security))
--- 613,618 ----
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
new file mode 100644
index 5bf595c..6de266e
*** a/src/backend/utils/init/miscinit.c
--- b/src/backend/utils/init/miscinit.c
*************** GetAuthenticatedUserId(void)
*** 359,367 ****
* where the called functions are really supposed to be side-effect-free
* anyway, such as VACUUM/ANALYZE/REINDEX.
*
- * SECURITY_ROW_LEVEL_DISABLED indicates that we are inside an operation that
- * needs to bypass row level security checks, for example FK checks.
- *
* Unlike GetUserId, GetUserIdAndSecContext does *not* Assert that the current
* value of CurrentUserId is valid; nor does SetUserIdAndSecContext require
* the new value to be valid. In fact, these routines had better not
--- 359,364 ----
*************** InSecurityRestrictedOperation(void)
*** 404,418 ****
return (SecurityRestrictionContext & SECURITY_RESTRICTED_OPERATION) != 0;
}
- /*
- * InRowLevelSecurityDisabled - are we inside a RLS-disabled operation?
- */
- bool
- InRowLevelSecurityDisabled(void)
- {
- return (SecurityRestrictionContext & SECURITY_ROW_LEVEL_DISABLED) != 0;
- }
-
/*
* These are obsolete versions of Get/SetUserIdAndSecContext that are
--- 401,406 ----
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
new file mode 100644
index 8ebf424..c941b42
*** a/src/backend/utils/misc/guc.c
--- b/src/backend/utils/misc/guc.c
*************** static struct config_enum ConfigureNames
*** 3633,3639 ****
{
{"row_security", PGC_USERSET, CONN_AUTH_SECURITY,
gettext_noop("Enable row security."),
! gettext_noop("When enabled, row security will be applied to all users.")
},
&row_security,
ROW_SECURITY_ON, row_security_options,
--- 3633,3640 ----
{
{"row_security", PGC_USERSET, CONN_AUTH_SECURITY,
gettext_noop("Enable row security."),
! gettext_noop("When enabled, row security will be applied to all users."),
! GUC_NOT_WHILE_SEC_REST
},
&row_security,
ROW_SECURITY_ON, row_security_options,
diff --git a/src/backend/utils/misc/rls.c b/src/backend/utils/misc/rls.c
new file mode 100644
index 7b8d51d..a94b9a7
*** a/src/backend/utils/misc/rls.c
--- b/src/backend/utils/misc/rls.c
*************** check_enable_rls(Oid relid, Oid checkAsU
*** 63,75 ****
if (relid < FirstNormalObjectId)
return RLS_NONE;
- /*
- * Check if we have been told to explicitly skip RLS (perhaps because this
- * is a foreign key check)
- */
- if (InRowLevelSecurityDisabled())
- return RLS_NONE;
-
tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
if (!HeapTupleIsValid(tuple))
return RLS_NONE;
--- 63,68 ----
*************** check_enable_rls(Oid relid, Oid checkAsU
*** 99,105 ****
* environment (in this case, what the current values of user_id and
* row_security are).
*/
! if (row_security != ROW_SECURITY_FORCE
&& (pg_class_ownercheck(relid, user_id)))
return RLS_NONE_ENV;
--- 92,98 ----
* environment (in this case, what the current values of user_id and
* row_security are).
*/
! if ((InLocalUserIdChange() || row_security != ROW_SECURITY_FORCE)
&& (pg_class_ownercheck(relid, user_id)))
return RLS_NONE_ENV;
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
new file mode 100644
index e0cc69f..80ac732
*** a/src/include/miscadmin.h
--- b/src/include/miscadmin.h
*************** extern int trace_recovery(int trace_leve
*** 286,292 ****
/* flags to be OR'd to form sec_context */
#define SECURITY_LOCAL_USERID_CHANGE 0x0001
#define SECURITY_RESTRICTED_OPERATION 0x0002
- #define SECURITY_ROW_LEVEL_DISABLED 0x0004
extern char *DatabasePath;
--- 286,291 ----
*************** extern void GetUserIdAndSecContext(Oid *
*** 305,311 ****
extern void SetUserIdAndSecContext(Oid userid, int sec_context);
extern bool InLocalUserIdChange(void);
extern bool InSecurityRestrictedOperation(void);
- extern bool InRowLevelSecurityDisabled(void);
extern void GetUserIdAndContext(Oid *userid, bool *sec_def_context);
extern void SetUserIdAndContext(Oid userid, bool sec_def_context);
extern void InitializeSessionUserId(const char *rolename, Oid useroid);
--- 304,309 ----
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
new file mode 100644
index 90a0180..1c50f8d
*** a/src/include/utils/plancache.h
--- b/src/include/utils/plancache.h
*************** typedef struct CachedPlanSource
*** 111,117 ****
int num_custom_plans; /* number of plans included in total */
bool hasRowSecurity; /* planned with row security? */
int row_security_env; /* row security setting when planned */
- bool rowSecurityDisabled; /* is row security disabled? */
} CachedPlanSource;
/*
--- 111,116 ----
On Thu, Sep 10, 2015 at 03:23:13PM -0400, Stephen Frost wrote:
First off, thanks again for your review and comments on RLS. Hopefully
this addresses the last remaining open item from that review.
Item (3a), too, remains open.
* Noah Misch (noah@leadboat.com) wrote:
On Wed, Feb 25, 2015 at 11:37:24PM -0500, Stephen Frost wrote:
+ /* + * Row-level security should be disabled in the case where a foreign-key + * relation is queried to check existence of tuples that references the + * primary-key being modified. + */ + temp_sec_context = save_sec_context | SECURITY_LOCAL_USERID_CHANGE; + if (qkey->constr_queryno == RI_PLAN_CHECK_LOOKUPPK + || qkey->constr_queryno == RI_PLAN_CHECK_LOOKUPPK_FROM_PK + || qkey->constr_queryno == RI_PLAN_RESTRICT_DEL_CHECKREF + || qkey->constr_queryno == RI_PLAN_RESTRICT_UPD_CHECKREF) + temp_sec_context |= SECURITY_ROW_LEVEL_DISABLED;
Is SECURITY_ROW_LEVEL_DISABLED mode even needed? We switch users to the owner
of the FROM-clause table before running an RI query. That means use of this
mode can only matter under row_security=force, right? I would rest easier if
this mode went away, because it is a security landmine. If user code managed
to run in this mode, it would bypass every policy in the database. (I find no
such vulnerability today, because we use the mode only for parse analysis of
ri_triggers.c queries.)You're right, the reason for it to exist was to address the
'row_security = force' case. I spent a bit of time considering that and
have come up with the attached for consideration (which needs additional
work before being committed, but should get the idea across clearly).This removes the notion of SECURITY_ROW_LEVEL_DISABLED entirely and
instead ignores 'row_security = force' if InLocalUserIdChange() is true.
The biggest drawback that I can see to this is that users won't be able
to set the row_security GUC inside of a security definer function. I
can imagine use-cases where someone might want to change it in a
security definer function but it doesn't seem like they're valuable
enough to counter your argument (which I agree with) that
SECURITY_ROW_LEVEL_DISABLED is a security landmine.
SECURITY DEFINER execution blocks SET ROLE, SET SESSION AUTHORIZATION, and
sometimes "GRANT role1 TO role2". Otherwise, it works like regular execution.
Adding exceptions, particularly silent behavior changes as opposed to hard
errors, is a big deal.
When I wrote the 2015-07-03 review, I had in mind to just let policies run
normally during referential integrity checks. Table owners, which can already
break referential integrity with triggers and rules, would be responsible for
crafting policies accordingly.
Pondering it afresh this week, I see now that row_security=force itself is the
problem. It's a new instance of the maligned "behavior-changing GUC".
Function authors will not consistently test the row_security=force case, and
others can run the functions under row_security=force and get novel results.
This poses a reliability and security threat. While row_security=force is
handy for testing, visiting a second role for testing purposes is a fine
replacement. Let's remove "force", making row_security a config_bool. If
someone later desires to revive the capability, a DDL-specified property of
each policy would be free from these problems.
On a related topic, check_enable_rls() has two kinds of RLS bypass semantics.
Under row_security=off|on, superusers bypass every policy, and table owners
bypass policies of their own tables. Under row_security=off only, roles with
BYPASSRLS privilege additionally bypass every policy; BYPASSRLS doesn't affect
semantics under row_security=on. Is that distinction a good thing? Making
BYPASSRLS GUC-independent, like superuser bypass, would simplify things for
the user and would make row_security no longer a behavior-changing GUC.
row_security would come to mean simply "if a policy would otherwise attach to
one of my queries, raise an error instead." Again, if you have cause to
toggle BYPASSRLS, use multiple roles. Implementing that with GUCs creates
harder problems.
In summary, I propose to remove row_security=force and make BYPASSRLS
effective under any setting of the row_security GUC.
Some past, brief discussion leading to the current semantics:
/messages/by-id/CA+TgmoYA=uixXmN390SFgfQgVmLL-As5bJaL0oM7yrpPVwNPxQ@mail.gmail.com
/messages/by-id/CAKRt6CQnghzWUGwb5Pkwg5gfXwd+-joy8MmMEnqh+O6vpLYzfA@mail.gmail.com
/messages/by-id/20150729130927.GS3587@tamriel.snowman.net
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Mon, Sep 14, 2015 at 3:29 AM, Noah Misch <noah@leadboat.com> wrote:
SECURITY DEFINER execution blocks SET ROLE, SET SESSION AUTHORIZATION, and
sometimes "GRANT role1 TO role2". Otherwise, it works like regular execution.
Adding exceptions, particularly silent behavior changes as opposed to hard
errors, is a big deal.
Yeah, that does seem possibly surprising...
Pondering it afresh this week, I see now that row_security=force itself is the
problem. It's a new instance of the maligned "behavior-changing GUC".
Function authors will not consistently test the row_security=force case, and
others can run the functions under row_security=force and get novel results.
This poses a reliability and security threat. While row_security=force is
handy for testing, visiting a second role for testing purposes is a fine
replacement. Let's remove "force", making row_security a config_bool. If
someone later desires to revive the capability, a DDL-specified property of
each policy would be free from these problems.
...but I'm not sure I like this, either. Without row_security=force,
it's hard for a table owner to test their policy, unless they have the
ability to assume some other user ID, which some won't. If someone
puts in place a policy which they believe secures their data properly
but which actually does not, and they are unable to test it properly
for lack of this setting, that too will be a security hole. We will
be able to attribute it to user error rather than product defect, but
that will be cold comfort to the person whose sensitive data has been
leaked.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Mon, Sep 14, 2015 at 07:30:33AM -0400, Robert Haas wrote:
On Mon, Sep 14, 2015 at 3:29 AM, Noah Misch <noah@leadboat.com> wrote:
Pondering it afresh this week, I see now that row_security=force itself is the
problem. It's a new instance of the maligned "behavior-changing GUC".
Function authors will not consistently test the row_security=force case, and
others can run the functions under row_security=force and get novel results.
This poses a reliability and security threat. While row_security=force is
handy for testing, visiting a second role for testing purposes is a fine
replacement. Let's remove "force", making row_security a config_bool. If
someone later desires to revive the capability, a DDL-specified property of
each policy would be free from these problems.
[A variation on that idea is "ALTER TABLE foo FORCE ROW LEVEL SECURITY",
affecting all policies of one table rather than individual policies.]
...but I'm not sure I like this, either. Without row_security=force,
it's hard for a table owner to test their policy, unless they have the
ability to assume some other user ID, which some won't. If someone
puts in place a policy which they believe secures their data properly
but which actually does not, and they are unable to test it properly
for lack of this setting, that too will be a security hole. We will
be able to attribute it to user error rather than product defect, but
that will be cold comfort to the person whose sensitive data has been
leaked.
The testing capability is nice, all else being equal. Your scenario entails a
data custodian wishing to test with row_security=force but willing to entrust
sensitive data to an untested policy. It also requires a DBA unwilling to
furnish test accounts to custodians of sensitive data. With or without
row_security=force, such a team is on the outer perimeter of the audience able
to benefit from RLS. Nonetheless, I'd welcome a replacement test aid.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Noah Misch <noah@leadboat.com> writes:
On Mon, Sep 14, 2015 at 07:30:33AM -0400, Robert Haas wrote:
...but I'm not sure I like this, either. Without row_security=force,
it's hard for a table owner to test their policy, unless they have the
ability to assume some other user ID, which some won't. If someone
puts in place a policy which they believe secures their data properly
but which actually does not, and they are unable to test it properly
for lack of this setting, that too will be a security hole. We will
be able to attribute it to user error rather than product defect, but
that will be cold comfort to the person whose sensitive data has been
leaked.
The testing capability is nice, all else being equal. Your scenario entails a
data custodian wishing to test with row_security=force but willing to entrust
sensitive data to an untested policy. It also requires a DBA unwilling to
furnish test accounts to custodians of sensitive data. With or without
row_security=force, such a team is on the outer perimeter of the audience able
to benefit from RLS. Nonetheless, I'd welcome a replacement test aid.
I have to say I'm with Noah on this. I do not think we should create
potential security holes to help users with uncooperative DBAs. Those
users have got problems far worse than whether they can test their RLS
policies; or in other words, what in God's name are you doing storing
sensitive data in a system with a hostile DBA?
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Tue, Sep 15, 2015 at 1:00 AM, Noah Misch <noah@leadboat.com> wrote:
...but I'm not sure I like this, either. Without row_security=force,
it's hard for a table owner to test their policy, unless they have the
ability to assume some other user ID, which some won't. If someone
puts in place a policy which they believe secures their data properly
but which actually does not, and they are unable to test it properly
for lack of this setting, that too will be a security hole. We will
be able to attribute it to user error rather than product defect, but
that will be cold comfort to the person whose sensitive data has been
leaked.The testing capability is nice, all else being equal. Your scenario entails a
data custodian wishing to test with row_security=force but willing to entrust
sensitive data to an untested policy.
That's not really accurate. You can test the policy first and only
afterwords GRANT access to others.
It also requires a DBA unwilling to
furnish test accounts to custodians of sensitive data. With or without
row_security=force, such a team is on the outer perimeter of the audience able
to benefit from RLS. Nonetheless, I'd welcome a replacement test aid.
I can't argue with that, I suppose, but I think row_security=force is
a pretty useful convenience. If we must remove it, so be it, but I'd
be a little sad.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Robert Haas <robertmhaas@gmail.com> writes:
On Tue, Sep 15, 2015 at 1:00 AM, Noah Misch <noah@leadboat.com> wrote:
It also requires a DBA unwilling to
furnish test accounts to custodians of sensitive data. With or without
row_security=force, such a team is on the outer perimeter of the audience able
to benefit from RLS. Nonetheless, I'd welcome a replacement test aid.
I can't argue with that, I suppose, but I think row_security=force is
a pretty useful convenience. If we must remove it, so be it, but I'd
be a little sad.
Keep in mind that if you have an uncooperative DBA on your production
system, you can always test your policy to your heart's content on a
playpen installation. In fact, most people would consider that good
engineering practice anyway, rather than pushing untested code directly
into production.
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 09/15/2015 10:58 AM, Robert Haas wrote:
I can't argue with that, I suppose, but I think row_security=force is
a pretty useful convenience. If we must remove it, so be it, but I'd
be a little sad.
There are use cases where row_security=force will be set in production
environments, not only in testing. I would be very strongly opposed to
removing the ability to force RLS from being applied to owners and
superusers, and in fact think we should figure out how to make changing
row_security, once it is set, more difficult.
Joe
--
Crunchy Data - http://crunchydata.com
PostgreSQL Support for Secure Enterprises
Consulting, Training, & Open Source Development
On 09/15/2015 11:10 AM, Tom Lane wrote:
Robert Haas <robertmhaas@gmail.com> writes:
On Tue, Sep 15, 2015 at 1:00 AM, Noah Misch <noah@leadboat.com> wrote:
It also requires a DBA unwilling to
furnish test accounts to custodians of sensitive data. With or without
row_security=force, such a team is on the outer perimeter of the audience able
to benefit from RLS. Nonetheless, I'd welcome a replacement test aid.I can't argue with that, I suppose, but I think row_security=force is
a pretty useful convenience. If we must remove it, so be it, but I'd
be a little sad.Keep in mind that if you have an uncooperative DBA on your production
system, you can always test your policy to your heart's content on a
playpen installation. In fact, most people would consider that good
engineering practice anyway, rather than pushing untested code directly
into production.
That's exactly right. We should provide flexibility for testing in test
environments, and also the ability to lock things down tight in production.
Joe
--
Crunchy Data - http://crunchydata.com
PostgreSQL Support for Secure Enterprises
Consulting, Training, & Open Source Development
Joe Conway <mail@joeconway.com> writes:
On 09/15/2015 10:58 AM, Robert Haas wrote:
I can't argue with that, I suppose, but I think row_security=force is
a pretty useful convenience. If we must remove it, so be it, but I'd
be a little sad.
There are use cases where row_security=force will be set in production
environments, not only in testing.
What cases, exactly, and is there another way to solve those problems?
I concur with Noah's feeling that allowing security behavior to depend on
a GUC is risky.
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 09/15/2015 12:53 PM, Tom Lane wrote:
Joe Conway <mail@joeconway.com> writes:
There are use cases where row_security=force will be set in production
environments, not only in testing.What cases, exactly, and is there another way to solve those problems?
I concur with Noah's feeling that allowing security behavior to depend on
a GUC is risky.
There are environments where there is a strong desire to use RLS to
control access in production, even for table owners and superusers.
Obviously there are more steps needed to completely achieve this level
of control, but removing the ability to force row security is going in
the wrong direction. Noah's suggestion of using a per table attribute
would work -- in fact I like the idea of that better than using the
current GUC.
Joe
--
Crunchy Data - http://crunchydata.com
PostgreSQL Support for Secure Enterprises
Consulting, Training, & Open Source Development
On Tue, Sep 15, 2015 at 2:26 PM, Joe Conway <mail@joeconway.com> wrote:
On 09/15/2015 12:53 PM, Tom Lane wrote:
Joe Conway <mail@joeconway.com> writes:
There are use cases where row_security=force will be set in production
environments, not only in testing.What cases, exactly, and is there another way to solve those problems?
I concur with Noah's feeling that allowing security behavior to depend on
a GUC is risky.There are environments where there is a strong desire to use RLS to
control access in production, even for table owners and superusers.
Obviously there are more steps needed to completely achieve this level
of control, but removing the ability to force row security is going in
the wrong direction. Noah's suggestion of using a per table attribute
would work -- in fact I like the idea of that better than using the
current GUC.
FWIW, I also concur with a per table attribute for this purpose. In
fact, I think I really like the per-table flexibility over an
'all-or-nothing' approach better too.
I do, however, think that it makes it a bit more difficult for
testing, specifically, I'm not sure how much I like the idea of
'altering' a table so that it can be tested, but that might a price
worth paying for security sake.
I could also see a potential gap in such approach. Specifically, I
could see a case were there are two separate roles, one that is
entrusted with defining the policies but not able to create/modify
tables, and one with the opposite capability (I understand this to be
a fairly common use-case, at least at a system level). Since you
can't GRANT 'alter' rights to the table, then obviously the policy
definer would have to either be the owner of the table or a member of
the role that owns it, right? Given that, if by definition the policy
definer is not allowed to do anything other than define policies, then
obviously putting such a role in the table owners group would allow it
to do much more, correct?
-Adam
--
Adam Brightwell - adam.brightwell@crunchydatasolutions.com
Database Engineer - www.crunchydatasolutions.com
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
I could also see a potential gap in such approach. Specifically, I
could see a case were there are two separate roles, one that is
entrusted with defining the policies but not able to create/modify
tables, and one with the opposite capability (I understand this to be
a fairly common use-case, at least at a system level). Since you
can't GRANT 'alter' rights to the table, then obviously the policy
definer would have to either be the owner of the table or a member of
the role that owns it, right? Given that, if by definition the policy
definer is not allowed to do anything other than define policies, then
obviously putting such a role in the table owners group would allow it
to do much more, correct?
Actually, disregard, I forgot about "You must be the owner of a table
to create or change policies for it." So that would obviously negate
my concern.
-Adam
--
Adam Brightwell - adam.brightwell@crunchydatasolutions.com
Database Engineer - www.crunchydatasolutions.com
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Tue, Sep 15, 2015 at 03:18:21PM -0400, Adam Brightwell wrote:
On Tue, Sep 15, 2015 at 2:26 PM, Joe Conway <mail@joeconway.com> wrote:
Joe Conway <mail@joeconway.com> writes:
There are use cases where row_security=force will be set in production
environments, not only in testing.
Noah's suggestion of using a per table attribute
would work -- in fact I like the idea of that better than using the
current GUC.FWIW, I also concur with a per table attribute for this purpose. In
fact, I think I really like the per-table flexibility over an
'all-or-nothing' approach better too.
Great. Robert, does that work for you, too? If so, this sub-thread is
looking at three patches:
1. remove row_security=force
2. remove SECURITY_ROW_LEVEL_DISABLED; make ri_triggers.c subject to policies
3. add DDL-controlled, per-table policy forcing
They ought to land in that order. PostgreSQL 9.5 would need at least (1) and
(2); would RLS experts find it beneficial for me to take care of those?
Thanks,
nm
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
2. remove SECURITY_ROW_LEVEL_DISABLED; make ri_triggers.c subject to policies
I believe this one has already been addressed by Stephen
(20150910192313.GT3685@tamriel.snowman.net)? Are there further
considerations for his proposed changes?
-Adam
--
Adam Brightwell - adam.brightwell@crunchydatasolutions.com
Database Engineer - www.crunchydatasolutions.com
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Fri, Sep 18, 2015 at 2:07 AM, Noah Misch <noah@leadboat.com> wrote:
On Tue, Sep 15, 2015 at 03:18:21PM -0400, Adam Brightwell wrote:
On Tue, Sep 15, 2015 at 2:26 PM, Joe Conway <mail@joeconway.com> wrote:
Joe Conway <mail@joeconway.com> writes:
There are use cases where row_security=force will be set in production
environments, not only in testing.Noah's suggestion of using a per table attribute
would work -- in fact I like the idea of that better than using the
current GUC.FWIW, I also concur with a per table attribute for this purpose. In
fact, I think I really like the per-table flexibility over an
'all-or-nothing' approach better too.Great. Robert, does that work for you, too?
Yes, that seems like a fine design from my point of view.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 09/18/2015 01:07 AM, Noah Misch wrote:
Great. Robert, does that work for you, too? If so, this sub-thread is
looking at three patches:1. remove row_security=force
2. remove SECURITY_ROW_LEVEL_DISABLED; make ri_triggers.c subject to policies
3. add DDL-controlled, per-table policy forcingThey ought to land in that order. PostgreSQL 9.5 would need at least (1) and
(2); would RLS experts find it beneficial for me to take care of those?
That would be awesome, but I would say that if we do #1 & 2 for 9.5, we
also need #3.
Joe
--
Crunchy Data - http://crunchydata.com
PostgreSQL Support for Secure Enterprises
Consulting, Training, & Open Source Development
1. remove row_security=force
2. remove SECURITY_ROW_LEVEL_DISABLED; make ri_triggers.c subject to policies
3. add DDL-controlled, per-table policy forcingThey ought to land in that order. PostgreSQL 9.5 would need at least (1) and
(2); would RLS experts find it beneficial for me to take care of those?That would be awesome, but I would say that if we do #1 & 2 for 9.5, we
also need #3.
Agreed. Please let me know if there is anything I can do to help.
-Adam
--
Adam Brightwell - adam.brightwell@crunchydatasolutions.com
Database Engineer - www.crunchydatasolutions.com
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 09/18/2015 09:25 AM, Adam Brightwell wrote:
1. remove row_security=force
2. remove SECURITY_ROW_LEVEL_DISABLED; make ri_triggers.c subject to policies
3. add DDL-controlled, per-table policy forcingThey ought to land in that order. PostgreSQL 9.5 would need at least (1) and
(2); would RLS experts find it beneficial for me to take care of those?That would be awesome, but I would say that if we do #1 & 2 for 9.5, we
also need #3.Agreed. Please let me know if there is anything I can do to help.
Yes, same here.
Joe
--
Crunchy Data - http://crunchydata.com
PostgreSQL Support for Secure Enterprises
Consulting, Training, & Open Source Development
On Fri, Sep 18, 2015 at 09:01:15AM -0400, Adam Brightwell wrote:
2. remove SECURITY_ROW_LEVEL_DISABLED; make ri_triggers.c subject to policies
I believe this one has already been addressed by Stephen
(20150910192313.GT3685@tamriel.snowman.net)? Are there further
considerations for his proposed changes?
Right. I'll use that patch, minus the bits examining InLocalUserIdChange().
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
* Joe Conway (mail@joeconway.com) wrote:
On 09/18/2015 01:07 AM, Noah Misch wrote:
Great. Robert, does that work for you, too? If so, this sub-thread is
looking at three patches:1. remove row_security=force
2. remove SECURITY_ROW_LEVEL_DISABLED; make ri_triggers.c subject to policies
3. add DDL-controlled, per-table policy forcingThey ought to land in that order. PostgreSQL 9.5 would need at least (1) and
(2); would RLS experts find it beneficial for me to take care of those?That would be awesome, but I would say that if we do #1 & 2 for 9.5, we
also need #3.
Agreed on all of the above.
Thanks!
Stephen
On Sun, Sep 20, 2015 at 5:35 PM, Stephen Frost <sfrost@snowman.net> wrote:
* Joe Conway (mail@joeconway.com) wrote:
On 09/18/2015 01:07 AM, Noah Misch wrote:
Great. Robert, does that work for you, too? If so, this sub-thread is
looking at three patches:1. remove row_security=force
2. remove SECURITY_ROW_LEVEL_DISABLED; make ri_triggers.c subject to policies
3. add DDL-controlled, per-table policy forcingThey ought to land in that order. PostgreSQL 9.5 would need at least (1) and
(2); would RLS experts find it beneficial for me to take care of those?That would be awesome, but I would say that if we do #1 & 2 for 9.5, we
also need #3.Agreed on all of the above.
Well, then, we should get cracking. beta1 is coming soon, and it
would be best if this were done before then.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Robert Haas <robertmhaas@gmail.com> writes:
On Sun, Sep 20, 2015 at 5:35 PM, Stephen Frost <sfrost@snowman.net> wrote:
* Joe Conway (mail@joeconway.com) wrote:
That would be awesome, but I would say that if we do #1 & 2 for 9.5, we
also need #3.
Agreed on all of the above.
Well, then, we should get cracking. beta1 is coming soon, and it
would be best if this were done before then.
I'd say it's *necessary*. We're not adding new features after beta1.
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Fri, Sep 18, 2015 at 09:27:13AM -0500, Joe Conway wrote:
On 09/18/2015 09:25 AM, Adam Brightwell wrote:
1. remove row_security=force
2. remove SECURITY_ROW_LEVEL_DISABLED; make ri_triggers.c subject to policies
3. add DDL-controlled, per-table policy forcingThey ought to land in that order. PostgreSQL 9.5 would need at least (1) and
(2); would RLS experts find it beneficial for me to take care of those?
Done.
That would be awesome, but I would say that if we do #1 & 2 for 9.5, we
also need #3.
Understood.
Agreed. Please let me know if there is anything I can do to help.
Yes, same here.
Thanks. https://wiki.postgresql.org/wiki/PostgreSQL_9.5_Open_Items is the big
board of things needing work. These items are long-idle:
- 84 days: UPSERT on partition
- 81 days: Refactoring speculative insertion with unique indexes a little
- 49 days: Arguable RLS security bug, EvalPlanQual() paranoia
If you grab one or more of those and figure out what it/they need to get
moving again, that would help move us toward 9.5 final. (Don't feel the need
to limit yourself to those three, but they are the low-hanging fruit.)
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Mon, Sep 14, 2015 at 03:29:16AM -0400, Noah Misch wrote:
On a related topic, check_enable_rls() has two kinds of RLS bypass semantics.
Under row_security=off|on, superusers bypass every policy, and table owners
bypass policies of their own tables. Under row_security=off only, roles with
BYPASSRLS privilege additionally bypass every policy; BYPASSRLS doesn't affect
semantics under row_security=on. Is that distinction a good thing? Making
BYPASSRLS GUC-independent, like superuser bypass, would simplify things for
the user and would make row_security no longer a behavior-changing GUC.
row_security would come to mean simply "if a policy would otherwise attach to
one of my queries, raise an error instead." Again, if you have cause to
toggle BYPASSRLS, use multiple roles. Implementing that with GUCs creates
harder problems.
Having pondered this further in the course of finalizing commit 537bd17,
arming BYPASSRLS independent of the row_security GUC is indeed a good thing.
Right now, if a BYPASSRLS user creates a SECURITY DEFINER function, any caller
can change that function's behavior by toggling the GUC. Users won't test
accordingly; better to have just one success-case behavior.
On Wed, Jul 29, 2015 at 09:09:27AM -0400, Stephen Frost wrote:
For superuser (the only similar precedent that we have, I believe), we
go based on the view owner, but that isn't quite the same as BYPASSRLS.The reason this doesn't hold is that you have to use a combination of
BYPASSRLS and row_security=off to actually bypass RLS, unlike the
superuser role attribute which is just always "on" if you've got it. If
having BYPASSRLS simply always meant "don't do any RLS" then we could
use the superuser precedent to use what the view owner has, but at least
for my part, I'm a lot happier with BYPASSRLS and row_security than with
superuser and would rather we continue in that direction, where the user
has the choice of if they want their role attribute to be in effect or
not.
If I make BYPASSRLS GUC-independent, I should then also make it take effect
when the BYPASSRLS role owns a view. Barring objections, I will change both.
I do share your wish for an ability to suppress privileges temporarily. I
have no specific design in mind, but privilege activation and suppression
should be subject to the approval of roles affected. GUCs probably can't
serve here; apart from the grandfathered search_path, functions can ignore
them. GUCs are mostly a property of the whole session.
By the way, is there a reason for RI_Initial_Check() to hard-code the rules
for RLS enablement instead of calling check_enable_rls(..., InvalidOid, true)
twice? I refer to this code:
/*
* Also punt if RLS is enabled on either table unless this role has the
* bypassrls right or is the table owner of the table(s) involved which
* have RLS enabled.
*/
if (!has_bypassrls_privilege(GetUserId()) &&
((pk_rel->rd_rel->relrowsecurity &&
!pg_class_ownercheck(pkrte->relid, GetUserId())) ||
(fk_rel->rd_rel->relrowsecurity &&
!pg_class_ownercheck(fkrte->relid, GetUserId()))))
return false;
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
* Noah Misch (noah@leadboat.com) wrote:
Right now, if a BYPASSRLS user creates a SECURITY DEFINER function, any caller
can change that function's behavior by toggling the GUC. Users won't test
accordingly; better to have just one success-case behavior.
I agree that's not good, though the function definer could set the
row_security GUC on the function, no? Similar to how we encourage
setting of search_path, we should be encouraging a similar approach to
anything which might be security relevant.
On Wed, Jul 29, 2015 at 09:09:27AM -0400, Stephen Frost wrote:
For superuser (the only similar precedent that we have, I believe), we
go based on the view owner, but that isn't quite the same as BYPASSRLS.The reason this doesn't hold is that you have to use a combination of
BYPASSRLS and row_security=off to actually bypass RLS, unlike the
superuser role attribute which is just always "on" if you've got it. If
having BYPASSRLS simply always meant "don't do any RLS" then we could
use the superuser precedent to use what the view owner has, but at least
for my part, I'm a lot happier with BYPASSRLS and row_security than with
superuser and would rather we continue in that direction, where the user
has the choice of if they want their role attribute to be in effect or
not.If I make BYPASSRLS GUC-independent, I should then also make it take effect
when the BYPASSRLS role owns a view. Barring objections, I will change both.
I agree that if it's GUC-independent then it should operate the same as
superuser does for views and security definer functions.
On the one hand, I don't like that BYPASSRLS roles will now behave
differently from non-BYPASSRLS roles, but on the other hand, the above
isn't good and having BYPASSRLS always enabled may make individuals shy
away from giving it out except when strictly necessary and treat it more
similar to superuser, which would be a good thing.
I do share your wish for an ability to suppress privileges temporarily. I
have no specific design in mind, but privilege activation and suppression
should be subject to the approval of roles affected. GUCs probably can't
serve here; apart from the grandfathered search_path, functions can ignore
them. GUCs are mostly a property of the whole session.
Perhaps GUCs won't work, but they own a pretty handy namespace
(SET X = Y) and we are able to attach specific GUC settings to
functions already. I don't like the idea that we'd invent a whole new
syntax or bits of grammar to do the same for whatever approach we come
up to for suppressing privileges temporarily (such as in SECURITY
DEFINER functions). The odd case here is really views, since they
operate somewhere inbetween regular queries and security definer
functions, regarding permissions.
By the way, is there a reason for RI_Initial_Check() to hard-code the rules
for RLS enablement instead of calling check_enable_rls(..., InvalidOid, true)
twice? I refer to this code:
I don't see a reason for it now, though I recall one existing when the
code was originally written. That might have simply been a bit of extra
(though unnecessary) paranoia though, as returning 'false' is a safe
route.
Are you planning to handle the ALTER TABLE .. FORCE ROW SECURITY (#3) as
well? I have no complaints if so; just want to make sure we aren't
doing double-work during this crunch time and didn't see your name
listed next to it, but the nearby thread seemed to imply you were
looking at it.
One item which wasn't discussed, that I recall, is just how it will work
without SECURITY_ROW_LEVEL_DISABLED, or something similar, to
differentiate when internal referencial integrity queries are being run,
which should still bypass RLS (even in the FORCE ROW SECURITY case), and
when regular or SECURITY DEFINER originated queries are being run.
The concensus, as I understood it, was that removing the ability to do
SET ROW_SECURITY = force is good, but if done, we need to support
ALTER TABLE .. FORCE ROW SECURITY. I'm trying to figure out if that
means we end up not actually addressing the original concern you raised
regarding SECURITY_ROW_LEVEL_DISABLED.
Thanks!
Stephen
On Mon, Sep 21, 2015 at 09:30:15AM -0400, Stephen Frost wrote:
* Noah Misch (noah@leadboat.com) wrote:
Right now, if a BYPASSRLS user creates a SECURITY DEFINER function, any caller
can change that function's behavior by toggling the GUC. Users won't test
accordingly; better to have just one success-case behavior.I agree that's not good, though the function definer could set the
row_security GUC on the function, no? Similar to how we encourage
setting of search_path, we should be encouraging a similar approach to
anything which might be security relevant.
Functions can do that. New features should not mimic search_path in their
demands on SECURITY DEFINER authors.
Are you planning to handle the ALTER TABLE .. FORCE ROW SECURITY (#3) as
well? I have no complaints if so; just want to make sure we aren't
doing double-work during this crunch time and didn't see your name
listed next to it, but the nearby thread seemed to imply you were
looking at it.
I'm not.
One item which wasn't discussed, that I recall, is just how it will work
without SECURITY_ROW_LEVEL_DISABLED, or something similar, to
differentiate when internal referencial integrity queries are being run,
which should still bypass RLS (even in the FORCE ROW SECURITY case), and
when regular or SECURITY DEFINER originated queries are being run.
If the table owner enables FORCE ROW SECURITY, policies will affect
referential integrity queries. Choose policies accordingly. For example,
given only ON UPDATE NO ACTION constraints, it would be no problem to set
owner-affecting policies for INSERT, UPDATE and/or DELETE.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
* Noah Misch (noah@leadboat.com) wrote:
On Mon, Sep 21, 2015 at 09:30:15AM -0400, Stephen Frost wrote:
* Noah Misch (noah@leadboat.com) wrote:
Right now, if a BYPASSRLS user creates a SECURITY DEFINER function, any caller
can change that function's behavior by toggling the GUC. Users won't test
accordingly; better to have just one success-case behavior.I agree that's not good, though the function definer could set the
row_security GUC on the function, no? Similar to how we encourage
setting of search_path, we should be encouraging a similar approach to
anything which might be security relevant.Functions can do that. New features should not mimic search_path in their
demands on SECURITY DEFINER authors.
I'm not convinced that having such a limitation on new features would be
a good thing. What was missing here was a safe default.
Are you planning to handle the ALTER TABLE .. FORCE ROW SECURITY (#3) as
well? I have no complaints if so; just want to make sure we aren't
doing double-work during this crunch time and didn't see your name
listed next to it, but the nearby thread seemed to imply you were
looking at it.I'm not.
I'll work on it then, if we can get agreement as to how it will work.
One item which wasn't discussed, that I recall, is just how it will work
without SECURITY_ROW_LEVEL_DISABLED, or something similar, to
differentiate when internal referencial integrity queries are being run,
which should still bypass RLS (even in the FORCE ROW SECURITY case), and
when regular or SECURITY DEFINER originated queries are being run.If the table owner enables FORCE ROW SECURITY, policies will affect
referential integrity queries. Choose policies accordingly. For example,
given only ON UPDATE NO ACTION constraints, it would be no problem to set
owner-affecting policies for INSERT, UPDATE and/or DELETE.
Perhaps I'm not following correctly, but the above doesn't look correct
to me. An ON UPDATE NO ACTION constraint would run a query against the
referring table (which has FORCE ROW SECURITY set, perhaps by mistake
after a debugging session of the owner, with a policy that does not
allow any records to be seen by the owner), fail to find any rows, and
conclude that no error needs to be thrown, resulting in the referring
table having records which refer to keys in the referred-to table that
no longer exist (the UPDATE having changed them).
As a test, I hacked the pg_class_ownercheck() case in check_enable_rls()
to return RLS_ENABLED always. Then did this:
CREATE ROLE r1;
CREATE ROLE r2;
CREATE TABLE t1 (c1 INT PRIMARY KEY);
CREATE TABLE t2 (c1 INT REFERENCES t1 ON UPDATE NO ACTION);
ALTER TABLE t1 OWNER TO r1;
ALTER TABLE t2 OWNER TO r2;
GRANT SELECT ON t1 TO r2;
INSERT INTO t1 VALUES (1);
INSERT INTO t2 VALUES (1);
ALTER TABLE t2 ENABLE ROW LEVEL SECURITY;
UPDATE t1 SET c1 = 2;
ALTER TABLE t2 DISABLE ROW LEVEL SECURITY;
TABLE t1;
TABLE t2;
With results:
=*# TABLE t1;
c1
----
2
(1 row)
=*# TABLE t2;
c1
----
1
(1 row)
This would lead to trivial to cause (and likely hard to diagnose)
referential integrity data corruption issues. I find that a very hard
pill to swallow in favor of a theoretical concern that new code may open
avenues of exploit for a new security context mode to bypass RLS when
doing internal referential integrity checks. Further, it changes this
additional capability, which was agreed would be added to offset removal
of the 'row_security = force' option, from useful to downright
dangerous.
Hopefully, I'm simply misunderstanding your proposal for the
FORCE ROW LEVEL SECURITY option.
Thanks!
Stephen
On Tue, Sep 22, 2015 at 10:38:53AM -0400, Stephen Frost wrote:
* Noah Misch (noah@leadboat.com) wrote:
On Mon, Sep 21, 2015 at 09:30:15AM -0400, Stephen Frost wrote:
One item which wasn't discussed, that I recall, is just how it will work
without SECURITY_ROW_LEVEL_DISABLED, or something similar, to
differentiate when internal referencial integrity queries are being run,
which should still bypass RLS (even in the FORCE ROW SECURITY case), and
when regular or SECURITY DEFINER originated queries are being run.If the table owner enables FORCE ROW SECURITY, policies will affect
referential integrity queries. Choose policies accordingly. For example,
given only ON UPDATE NO ACTION constraints, it would be no problem to set
owner-affecting policies for INSERT, UPDATE and/or DELETE.Perhaps I'm not following correctly, but the above doesn't look correct
to me. An ON UPDATE NO ACTION constraint would run a query against the
referring table (which has FORCE ROW SECURITY set, perhaps by mistake
after a debugging session of the owner, with a policy that does not
allow any records to be seen by the owner), fail to find any rows, and
conclude that no error needs to be thrown, resulting in the referring
table having records which refer to keys in the referred-to table that
no longer exist (the UPDATE having changed them).
Yes, the table owner could define policies that thwart referential integrity.
This would lead to trivial to cause (and likely hard to diagnose)
referential integrity data corruption issues. I find that a very hard
pill to swallow in favor of a theoretical concern that new code may open
avenues of exploit for a new security context mode to bypass RLS when
doing internal referential integrity checks. Further, it changes this
additional capability, which was agreed would be added to offset removal
of the 'row_security = force' option, from useful to downright
dangerous.
In schema reviews, I will raise a red flag for use of this feature; the best
designs will instead use additional roles. I forecast that PostgreSQL would
fare better with no owner-constrained-by-RLS capability. Even so, others want
it, and FORCE ROW SECURITY would deliver it with an acceptable risk profile.
"SET row_security=force" was too risky, and not in a way particular to foreign
key constraints, because the session user chose row_security=force independent
of object owners. With FORCE ROW SECURITY, each table owner would make both
decisions. A foreign key constraint, plus a SELECT policy hiding rows from
the table owner, plus FORCE ROW SECURITY is one example of self-contradictory
policy design. That example is unexceptional amidst the countless ways a
table owner can get security policy wrong.
SECURITY_ROW_LEVEL_DISABLED could have been okay. I removed an incomplete
implementation (e.g. didn't affect CASCADE constraints). Writing a full one
would be a mammoth job, and for what? Setting the wrong SELECT policies can
disrupt _any_ application logic; no foreign keys or FORCE ROW SECURITY need be
involved. Protecting just foreign keys brings some value, but it will not
materially reduce the vigilance demanded of RLS policy authors and reviewers.
nm
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
* Noah Misch (noah@leadboat.com) wrote:
On Thu, Sep 10, 2015 at 03:23:13PM -0400, Stephen Frost wrote:
First off, thanks again for your review and comments on RLS. Hopefully
this addresses the last remaining open item from that review.Item (3a), too, remains open.
Provided I didn't break the buildfarm (keeping an eye on it), this has
been done.
Will update the open items wiki once it looks like the buildfarm is
happy.
Thanks!
Stephen
* Noah Misch (noah@leadboat.com) wrote:
On Tue, Sep 22, 2015 at 10:38:53AM -0400, Stephen Frost wrote:
This would lead to trivial to cause (and likely hard to diagnose)
referential integrity data corruption issues. I find that a very hard
pill to swallow in favor of a theoretical concern that new code may open
avenues of exploit for a new security context mode to bypass RLS when
doing internal referential integrity checks. Further, it changes this
additional capability, which was agreed would be added to offset removal
of the 'row_security = force' option, from useful to downright
dangerous.In schema reviews, I will raise a red flag for use of this feature; the best
designs will instead use additional roles. I forecast that PostgreSQL would
fare better with no owner-constrained-by-RLS capability. Even so, others want
it, and FORCE ROW SECURITY would deliver it with an acceptable risk profile.
I've attached a patch to implement it. It's not fully polished but it's
sufficient for comment, I believe. Additional comments, documentation
and regression tests are to be added, if we have agreement on the
grammer and implementation approach.
"SET row_security=force" was too risky, and not in a way particular to foreign
key constraints, because the session user chose row_security=force independent
of object owners. With FORCE ROW SECURITY, each table owner would make both
decisions. A foreign key constraint, plus a SELECT policy hiding rows from
the table owner, plus FORCE ROW SECURITY is one example of self-contradictory
policy design. That example is unexceptional amidst the countless ways a
table owner can get security policy wrong.
I agree that a table owner can get security policy wrong in any number
of ways and that having a FORCE RLS option at the table level is better
than the GUC.
SECURITY_ROW_LEVEL_DISABLED could have been okay. I removed an incomplete
implementation (e.g. didn't affect CASCADE constraints). Writing a full one
would be a mammoth job, and for what? Setting the wrong SELECT policies can
disrupt _any_ application logic; no foreign keys or FORCE ROW SECURITY need be
involved. Protecting just foreign keys brings some value, but it will not
materially reduce the vigilance demanded of RLS policy authors and reviewers.
I have a hard time with this. We're not talking about the application
logic in this case, we're talking about the guarantees which the
database is making to the user, be it an application or an individual.
I've included a patch (the second in the set attached) which adds a
SECURITY_NOFORCE_RLS bit which has a much tighter scope- it only applies
after the regular owner check is done. This reduces the risk of it
being set mistakenly dramatically, I believe. Further, the cascaded
case is correctly handled. This also needs more polish and regression
tests, but I wanted to solicit for comment before moving forward with
that since we don't have a consensus about if this should be done.
Thanks!
Stephen
Attachments:
rls-force.v1.patchtext/x-diff; charset=us-asciiDownload
From 32f0298403dcf3ea72c104fa9bddadfa75f3114e Mon Sep 17 00:00:00 2001
From: Stephen Frost <sfrost@snowman.net>
Date: Mon, 28 Sep 2015 11:28:15 -0400
Subject: [PATCH 1/2] ALTER TABLE .. FORCE ROW LEVEL SECURITY
To allow users to force RLS to always be applied, even for table owners,
add ALTER TABLE .. FORCE ROW LEVEL SECURITY.
Note that this ends up applying RLS even to referential integrity checks
and can therefore result in corruption.
---
doc/src/sgml/catalogs.sgml | 10 +++
doc/src/sgml/ref/alter_table.sgml | 17 +++++
src/backend/catalog/heap.c | 1 +
src/backend/commands/tablecmds.c | 68 ++++++++++++++++++++
src/backend/parser/gram.y | 14 +++++
src/backend/utils/misc/rls.c | 16 +++--
src/bin/pg_dump/pg_dump.c | 20 +++++-
src/bin/pg_dump/pg_dump.h | 1 +
src/bin/psql/describe.c | 44 +++++++------
src/include/catalog/pg_class.h | 72 +++++++++++-----------
src/include/nodes/parsenodes.h | 2 +
.../modules/test_ddl_deparse/test_ddl_deparse.c | 6 ++
12 files changed, 213 insertions(+), 58 deletions(-)
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index e8bd3a1..5c6a480 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -1972,6 +1972,16 @@
</row>
<row>
+ <entry><structfield>relforcerowsecurity</structfield></entry>
+ <entry><type>bool</type></entry>
+ <entry></entry>
+ <entry>
+ True if row level security (when enabled) will also apply to table owner; see
+ <link linkend="catalog-pg-policy"><structname>pg_policy</structname></link> catalog
+ </entry>
+ </row>
+
+ <row>
<entry><structfield>relispopulated</structfield></entry>
<entry><type>bool</type></entry>
<entry></entry>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 8e35cd9..aca40f5 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -61,6 +61,8 @@ ALTER TABLE ALL IN TABLESPACE <replaceable class="PARAMETER">name</replaceable>
ENABLE ALWAYS RULE <replaceable class="PARAMETER">rewrite_rule_name</replaceable>
DISABLE ROW LEVEL SECURITY
ENABLE ROW LEVEL SECURITY
+ FORCE ROW LEVEL SECURITY
+ NO FORCE ROW LEVEL SECURITY
CLUSTER ON <replaceable class="PARAMETER">index_name</replaceable>
SET WITHOUT CLUSTER
SET WITH OIDS
@@ -434,6 +436,21 @@ ALTER TABLE ALL IN TABLESPACE <replaceable class="PARAMETER">name</replaceable>
</varlistentry>
<varlistentry>
+ <term><literal>NO FORCE</literal>/<literal>FORCE ROW LEVEL SECURITY</literal></term>
+ <listitem>
+ <para>
+ These forms control the application of row security policies belonging
+ to the table when the user is the table owner. If enabled, row level
+ security policies will be applied when the user is the table owner. If
+ disabled (the default) then row level security will not be applied when
+ the user is the table owner.
+ See also
+ <xref linkend="SQL-CREATEPOLICY">.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
<term><literal>CLUSTER ON</literal></term>
<listitem>
<para>
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index d04e94d..7d7d062 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -802,6 +802,7 @@ InsertPgClassTuple(Relation pg_class_desc,
values[Anum_pg_class_relhasrules - 1] = BoolGetDatum(rd_rel->relhasrules);
values[Anum_pg_class_relhastriggers - 1] = BoolGetDatum(rd_rel->relhastriggers);
values[Anum_pg_class_relrowsecurity - 1] = BoolGetDatum(rd_rel->relrowsecurity);
+ values[Anum_pg_class_relforcerowsecurity - 1] = BoolGetDatum(rd_rel->relforcerowsecurity);
values[Anum_pg_class_relhassubclass - 1] = BoolGetDatum(rd_rel->relhassubclass);
values[Anum_pg_class_relispopulated - 1] = BoolGetDatum(rd_rel->relispopulated);
values[Anum_pg_class_relreplident - 1] = CharGetDatum(rd_rel->relreplident);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 126b119..b494dbf 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -419,6 +419,8 @@ static void ATExecReplicaIdentity(Relation rel, ReplicaIdentityStmt *stmt, LOCKM
static void ATExecGenericOptions(Relation rel, List *options);
static void ATExecEnableRowSecurity(Relation rel);
static void ATExecDisableRowSecurity(Relation rel);
+static void ATExecForceRowSecurity(Relation rel);
+static void ATExecNoForceRowSecurity(Relation rel);
static void copy_relation_data(SMgrRelation rel, SMgrRelation dst,
ForkNumber forkNum, char relpersistence);
@@ -2930,6 +2932,8 @@ AlterTableGetLockLevel(List *cmds)
case AT_SetNotNull:
case AT_EnableRowSecurity:
case AT_DisableRowSecurity:
+ case AT_ForceRowSecurity:
+ case AT_NoForceRowSecurity:
cmd_lockmode = AccessExclusiveLock;
break;
@@ -3351,6 +3355,8 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
case AT_DropOf: /* NOT OF */
case AT_EnableRowSecurity:
case AT_DisableRowSecurity:
+ case AT_ForceRowSecurity:
+ case AT_NoForceRowSecurity:
ATSimplePermissions(rel, ATT_TABLE);
/* These commands never recurse */
/* No command-specific prep needed */
@@ -3667,6 +3673,12 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab, Relation rel,
case AT_DisableRowSecurity:
ATExecDisableRowSecurity(rel);
break;
+ case AT_ForceRowSecurity:
+ ATExecForceRowSecurity(rel);
+ break;
+ case AT_NoForceRowSecurity:
+ ATExecNoForceRowSecurity(rel);
+ break;
case AT_GenericOptions:
ATExecGenericOptions(rel, (List *) cmd->def);
break;
@@ -11067,6 +11079,62 @@ ATExecDisableRowSecurity(Relation rel)
}
/*
+ * ALTER TABLE FORCE/NO FORCE ROW LEVEL SECURITY
+ */
+static void
+ATExecForceRowSecurity(Relation rel)
+{
+ Relation pg_class;
+ Oid relid;
+ HeapTuple tuple;
+
+ relid = RelationGetRelid(rel);
+
+ pg_class = heap_open(RelationRelationId, RowExclusiveLock);
+
+ tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
+
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for relation %u", relid);
+
+ ((Form_pg_class) GETSTRUCT(tuple))->relforcerowsecurity = true;
+ simple_heap_update(pg_class, &tuple->t_self, tuple);
+
+ /* keep catalog indexes current */
+ CatalogUpdateIndexes(pg_class, tuple);
+
+ heap_close(pg_class, RowExclusiveLock);
+ heap_freetuple(tuple);
+}
+
+static void
+ATExecNoForceRowSecurity(Relation rel)
+{
+ Relation pg_class;
+ Oid relid;
+ HeapTuple tuple;
+
+ relid = RelationGetRelid(rel);
+
+ /* Pull the record for this relation and update it */
+ pg_class = heap_open(RelationRelationId, RowExclusiveLock);
+
+ tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
+
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for relation %u", relid);
+
+ ((Form_pg_class) GETSTRUCT(tuple))->relforcerowsecurity = false;
+ simple_heap_update(pg_class, &tuple->t_self, tuple);
+
+ /* keep catalog indexes current */
+ CatalogUpdateIndexes(pg_class, tuple);
+
+ heap_close(pg_class, RowExclusiveLock);
+ heap_freetuple(tuple);
+}
+
+/*
* ALTER FOREIGN TABLE <name> OPTIONS (...)
*/
static void
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index fb84937..2168a08 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -2353,6 +2353,20 @@ alter_table_cmd:
n->subtype = AT_DisableRowSecurity;
$$ = (Node *)n;
}
+ /* ALTER TABLE <name> FORCE ROW LEVEL SECURITY */
+ | FORCE ROW LEVEL SECURITY
+ {
+ AlterTableCmd *n = makeNode(AlterTableCmd);
+ n->subtype = AT_ForceRowSecurity;
+ $$ = (Node *)n;
+ }
+ /* ALTER TABLE <name> NO FORCE ROW LEVEL SECURITY */
+ | NO FORCE ROW LEVEL SECURITY
+ {
+ AlterTableCmd *n = makeNode(AlterTableCmd);
+ n->subtype = AT_NoForceRowSecurity;
+ $$ = (Node *)n;
+ }
| alter_generic_options
{
AlterTableCmd *n = makeNode(AlterTableCmd);
diff --git a/src/backend/utils/misc/rls.c b/src/backend/utils/misc/rls.c
index c900c98..b4540ff 100644
--- a/src/backend/utils/misc/rls.c
+++ b/src/backend/utils/misc/rls.c
@@ -57,6 +57,7 @@ check_enable_rls(Oid relid, Oid checkAsUser, bool noError)
HeapTuple tuple;
Form_pg_class classform;
bool relrowsecurity;
+ bool relforcerowsecurity;
Oid user_id = checkAsUser ? checkAsUser : GetUserId();
/* Nothing to do for built-in relations */
@@ -70,6 +71,7 @@ check_enable_rls(Oid relid, Oid checkAsUser, bool noError)
classform = (Form_pg_class) GETSTRUCT(tuple);
relrowsecurity = classform->relrowsecurity;
+ relforcerowsecurity = classform->relforcerowsecurity;
ReleaseSysCache(tuple);
@@ -80,12 +82,18 @@ check_enable_rls(Oid relid, Oid checkAsUser, bool noError)
/*
* Check permissions
*
- * Table owners always bypass RLS. Note that superuser is always
- * considered an owner. Return RLS_NONE_ENV to indicate that this
- * decision depends on the environment (in this case, the user_id).
+ * Table owners always bypass RLS, unless the table has been set to
+ * FORCE ROW SECURITY. Note that superuser is always considered an
+ * owner. Return RLS_NONE_ENV to indicate that this decision depends
+ * on the environment (in this case, the user_id).
*/
if (pg_class_ownercheck(relid, user_id))
- return RLS_NONE_ENV;
+ {
+ if (relforcerowsecurity)
+ return RLS_ENABLED;
+ else
+ return RLS_NONE_ENV;
+ }
/*
* If the row_security GUC is 'off', check if the user has permission to
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 56c256d..e5be640 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -4540,6 +4540,7 @@ getTables(Archive *fout, DumpOptions *dopt, int *numTables)
int i_relhasindex;
int i_relhasrules;
int i_relrowsec;
+ int i_relforcerowsec;
int i_relhasoids;
int i_relfrozenxid;
int i_relminmxid;
@@ -4593,7 +4594,7 @@ getTables(Archive *fout, DumpOptions *dopt, int *numTables)
"(%s c.relowner) AS rolname, "
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
- "c.relrowsecurity, "
+ "c.relrowsecurity, c.relforcerowsecurity, "
"c.relfrozenxid, c.relminmxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"tc.relminmxid AS tminmxid, "
@@ -4635,6 +4636,7 @@ getTables(Archive *fout, DumpOptions *dopt, int *numTables)
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
"'f'::bool AS relrowsecurity, "
+ "'f'::bool AS relforcerowsecurity, "
"c.relfrozenxid, c.relminmxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"tc.relminmxid AS tminmxid, "
@@ -4676,6 +4678,7 @@ getTables(Archive *fout, DumpOptions *dopt, int *numTables)
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
"'f'::bool AS relrowsecurity, "
+ "'f'::bool AS relforcerowsecurity, "
"c.relfrozenxid, c.relminmxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"tc.relminmxid AS tminmxid, "
@@ -4717,6 +4720,7 @@ getTables(Archive *fout, DumpOptions *dopt, int *numTables)
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
"'f'::bool AS relrowsecurity, "
+ "'f'::bool AS relforcerowsecurity, "
"c.relfrozenxid, 0 AS relminmxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"0 AS tminmxid, "
@@ -4756,6 +4760,7 @@ getTables(Archive *fout, DumpOptions *dopt, int *numTables)
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
"'f'::bool AS relrowsecurity, "
+ "'f'::bool AS relforcerowsecurity, "
"c.relfrozenxid, 0 AS relminmxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"0 AS tminmxid, "
@@ -4794,6 +4799,7 @@ getTables(Archive *fout, DumpOptions *dopt, int *numTables)
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
"'f'::bool AS relrowsecurity, "
+ "'f'::bool AS relforcerowsecurity, "
"c.relfrozenxid, 0 AS relminmxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"0 AS tminmxid, "
@@ -4832,6 +4838,7 @@ getTables(Archive *fout, DumpOptions *dopt, int *numTables)
"c.relchecks, (c.reltriggers <> 0) AS relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
"'f'::bool AS relrowsecurity, "
+ "'f'::bool AS relforcerowsecurity, "
"c.relfrozenxid, 0 AS relminmxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"0 AS tminmxid, "
@@ -4870,6 +4877,7 @@ getTables(Archive *fout, DumpOptions *dopt, int *numTables)
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, relhasoids, "
"'f'::bool AS relrowsecurity, "
+ "'f'::bool AS relforcerowsecurity, "
"0 AS relfrozenxid, 0 AS relminmxid,"
"0 AS toid, "
"0 AS tfrozenxid, 0 AS tminmxid,"
@@ -4907,6 +4915,7 @@ getTables(Archive *fout, DumpOptions *dopt, int *numTables)
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, relhasoids, "
"'f'::bool AS relrowsecurity, "
+ "'f'::bool AS relforcerowsecurity, "
"0 AS relfrozenxid, 0 AS relminmxid,"
"0 AS toid, "
"0 AS tfrozenxid, 0 AS tminmxid,"
@@ -4940,6 +4949,7 @@ getTables(Archive *fout, DumpOptions *dopt, int *numTables)
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, relhasoids, "
"'f'::bool AS relrowsecurity, "
+ "'f'::bool AS relforcerowsecurity, "
"0 AS relfrozenxid, 0 AS relminmxid,"
"0 AS toid, "
"0 AS tfrozenxid, 0 AS tminmxid,"
@@ -4968,6 +4978,7 @@ getTables(Archive *fout, DumpOptions *dopt, int *numTables)
"relhasindex, relhasrules, "
"'t'::bool AS relhasoids, "
"'f'::bool AS relrowsecurity, "
+ "'f'::bool AS relforcerowsecurity, "
"0 AS relfrozenxid, 0 AS relminmxid,"
"0 AS toid, "
"0 AS tfrozenxid, 0 AS tminmxid,"
@@ -5006,6 +5017,7 @@ getTables(Archive *fout, DumpOptions *dopt, int *numTables)
"relhasindex, relhasrules, "
"'t'::bool AS relhasoids, "
"'f'::bool AS relrowsecurity, "
+ "'f'::bool AS relforcerowsecurity, "
"0 AS relfrozenxid, 0 AS relminmxid,"
"0 AS toid, "
"0 AS tfrozenxid, 0 AS tminmxid,"
@@ -5054,6 +5066,7 @@ getTables(Archive *fout, DumpOptions *dopt, int *numTables)
i_relhasindex = PQfnumber(res, "relhasindex");
i_relhasrules = PQfnumber(res, "relhasrules");
i_relrowsec = PQfnumber(res, "relrowsecurity");
+ i_relforcerowsec = PQfnumber(res, "relforcerowsecurity");
i_relhasoids = PQfnumber(res, "relhasoids");
i_relfrozenxid = PQfnumber(res, "relfrozenxid");
i_relminmxid = PQfnumber(res, "relminmxid");
@@ -5106,6 +5119,7 @@ getTables(Archive *fout, DumpOptions *dopt, int *numTables)
tblinfo[i].hasrules = (strcmp(PQgetvalue(res, i, i_relhasrules), "t") == 0);
tblinfo[i].hastriggers = (strcmp(PQgetvalue(res, i, i_relhastriggers), "t") == 0);
tblinfo[i].rowsec = (strcmp(PQgetvalue(res, i, i_relrowsec), "t") == 0);
+ tblinfo[i].forcerowsec = (strcmp(PQgetvalue(res, i, i_relforcerowsec), "t") == 0);
tblinfo[i].hasoids = (strcmp(PQgetvalue(res, i, i_relhasoids), "t") == 0);
tblinfo[i].relispopulated = (strcmp(PQgetvalue(res, i, i_relispopulated), "t") == 0);
tblinfo[i].relreplident = *(PQgetvalue(res, i, i_relreplident));
@@ -14412,6 +14426,10 @@ dumpTableSchema(Archive *fout, DumpOptions *dopt, TableInfo *tbinfo)
appendPQExpBuffer(q, "\nALTER TABLE ONLY %s SET WITH OIDS;\n",
fmtId(tbinfo->dobj.name));
+ if (tbinfo->forcerowsec)
+ appendPQExpBuffer(q, "\nALTER TABLE ONLY %s FORCE ROW LEVEL SECURITY;\n",
+ fmtId(tbinfo->dobj.name));
+
if (dopt->binary_upgrade)
binary_upgrade_extension_member(q, &tbinfo->dobj, labelq->data);
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index b40b816..3c64a82 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -211,6 +211,7 @@ typedef struct _tableInfo
bool hasrules; /* does it have any rules? */
bool hastriggers; /* does it have any triggers? */
bool rowsec; /* is row security enabled? */
+ bool forcerowsec; /* is row security forced? */
bool hasoids; /* does it have OIDs? */
uint32 frozenxid; /* for restore frozen xid */
uint32 minmxid; /* for restore min multi xid */
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 898f8b3..92ed6e2 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -1234,6 +1234,7 @@ describeOneTableDetails(const char *schemaname,
bool hasrules;
bool hastriggers;
bool rowsecurity;
+ bool forcerowsecurity;
bool hasoids;
Oid tablespace;
char *reloptions;
@@ -1259,8 +1260,8 @@ describeOneTableDetails(const char *schemaname,
{
printfPQExpBuffer(&buf,
"SELECT c.relchecks, c.relkind, c.relhasindex, c.relhasrules, "
- "c.relhastriggers, c.relrowsecurity, c.relhasoids, "
- "%s, c.reltablespace, "
+ "c.relhastriggers, c.relrowsecurity, c.relforcerowsecurity, "
+ "c.relhasoids, %s, c.reltablespace, "
"CASE WHEN c.reloftype = 0 THEN '' ELSE c.reloftype::pg_catalog.regtype::pg_catalog.text END, "
"c.relpersistence, c.relreplident\n"
"FROM pg_catalog.pg_class c\n "
@@ -1276,7 +1277,7 @@ describeOneTableDetails(const char *schemaname,
{
printfPQExpBuffer(&buf,
"SELECT c.relchecks, c.relkind, c.relhasindex, c.relhasrules, "
- "c.relhastriggers, false, c.relhasoids, "
+ "c.relhastriggers, false, false, c.relhasoids, "
"%s, c.reltablespace, "
"CASE WHEN c.reloftype = 0 THEN '' ELSE c.reloftype::pg_catalog.regtype::pg_catalog.text END, "
"c.relpersistence, c.relreplident\n"
@@ -1293,7 +1294,7 @@ describeOneTableDetails(const char *schemaname,
{
printfPQExpBuffer(&buf,
"SELECT c.relchecks, c.relkind, c.relhasindex, c.relhasrules, "
- "c.relhastriggers, false, c.relhasoids, "
+ "c.relhastriggers, false, false, c.relhasoids, "
"%s, c.reltablespace, "
"CASE WHEN c.reloftype = 0 THEN '' ELSE c.reloftype::pg_catalog.regtype::pg_catalog.text END, "
"c.relpersistence\n"
@@ -1310,7 +1311,7 @@ describeOneTableDetails(const char *schemaname,
{
printfPQExpBuffer(&buf,
"SELECT c.relchecks, c.relkind, c.relhasindex, c.relhasrules, "
- "c.relhastriggers, false, c.relhasoids, "
+ "c.relhastriggers, false, false, c.relhasoids, "
"%s, c.reltablespace, "
"CASE WHEN c.reloftype = 0 THEN '' ELSE c.reloftype::pg_catalog.regtype::pg_catalog.text END\n"
"FROM pg_catalog.pg_class c\n "
@@ -1326,7 +1327,7 @@ describeOneTableDetails(const char *schemaname,
{
printfPQExpBuffer(&buf,
"SELECT c.relchecks, c.relkind, c.relhasindex, c.relhasrules, "
- "c.relhastriggers, false, c.relhasoids, "
+ "c.relhastriggers, false, false, c.relhasoids, "
"%s, c.reltablespace\n"
"FROM pg_catalog.pg_class c\n "
"LEFT JOIN pg_catalog.pg_class tc ON (c.reltoastrelid = tc.oid)\n"
@@ -1341,7 +1342,7 @@ describeOneTableDetails(const char *schemaname,
{
printfPQExpBuffer(&buf,
"SELECT relchecks, relkind, relhasindex, relhasrules, "
- "reltriggers <> 0, false, relhasoids, "
+ "reltriggers <> 0, false, false, relhasoids, "
"%s, reltablespace\n"
"FROM pg_catalog.pg_class WHERE oid = '%s';",
(verbose ?
@@ -1352,7 +1353,7 @@ describeOneTableDetails(const char *schemaname,
{
printfPQExpBuffer(&buf,
"SELECT relchecks, relkind, relhasindex, relhasrules, "
- "reltriggers <> 0, false, relhasoids, "
+ "reltriggers <> 0, false, false, relhasoids, "
"'', reltablespace\n"
"FROM pg_catalog.pg_class WHERE oid = '%s';",
oid);
@@ -1361,7 +1362,7 @@ describeOneTableDetails(const char *schemaname,
{
printfPQExpBuffer(&buf,
"SELECT relchecks, relkind, relhasindex, relhasrules, "
- "reltriggers <> 0, false, relhasoids, "
+ "reltriggers <> 0, false, false, relhasoids, "
"'', ''\n"
"FROM pg_catalog.pg_class WHERE oid = '%s';",
oid);
@@ -1385,18 +1386,19 @@ describeOneTableDetails(const char *schemaname,
tableinfo.hasrules = strcmp(PQgetvalue(res, 0, 3), "t") == 0;
tableinfo.hastriggers = strcmp(PQgetvalue(res, 0, 4), "t") == 0;
tableinfo.rowsecurity = strcmp(PQgetvalue(res, 0, 5), "t") == 0;
- tableinfo.hasoids = strcmp(PQgetvalue(res, 0, 6), "t") == 0;
+ tableinfo.forcerowsecurity = strcmp(PQgetvalue(res, 0, 6), "t") == 0;
+ tableinfo.hasoids = strcmp(PQgetvalue(res, 0, 7), "t") == 0;
tableinfo.reloptions = (pset.sversion >= 80200) ?
- pg_strdup(PQgetvalue(res, 0, 7)) : NULL;
+ pg_strdup(PQgetvalue(res, 0, 8)) : NULL;
tableinfo.tablespace = (pset.sversion >= 80000) ?
- atooid(PQgetvalue(res, 0, 8)) : 0;
+ atooid(PQgetvalue(res, 0, 9)) : 0;
tableinfo.reloftype = (pset.sversion >= 90000 &&
- strcmp(PQgetvalue(res, 0, 9), "") != 0) ?
- pg_strdup(PQgetvalue(res, 0, 9)) : NULL;
+ strcmp(PQgetvalue(res, 0, 10), "") != 0) ?
+ pg_strdup(PQgetvalue(res, 0, 10)) : NULL;
tableinfo.relpersistence = (pset.sversion >= 90100) ?
- *(PQgetvalue(res, 0, 10)) : 0;
+ *(PQgetvalue(res, 0, 11)) : 0;
tableinfo.relreplident = (pset.sversion >= 90400) ?
- *(PQgetvalue(res, 0, 11)) : 'd';
+ *(PQgetvalue(res, 0, 12)) : 'd';
PQclear(res);
res = NULL;
@@ -2057,12 +2059,18 @@ describeOneTableDetails(const char *schemaname,
* there aren't policies, or RLS isn't enabled but there are
* policies
*/
- if (tableinfo.rowsecurity && tuples > 0)
+ if (tableinfo.rowsecurity && !tableinfo.forcerowsecurity && tuples > 0)
printTableAddFooter(&cont, _("Policies:"));
- if (tableinfo.rowsecurity && tuples == 0)
+ if (tableinfo.rowsecurity && tableinfo.forcerowsecurity && tuples > 0)
+ printTableAddFooter(&cont, _("Policies (Forced Row Security Enabled):"));
+
+ if (tableinfo.rowsecurity && !tableinfo.forcerowsecurity && tuples == 0)
printTableAddFooter(&cont, _("Policies (Row Security Enabled): (None)"));
+ if (tableinfo.rowsecurity && tableinfo.forcerowsecurity && tuples == 0)
+ printTableAddFooter(&cont, _("Policies (Forced Row Security Enabled): (None)"));
+
if (!tableinfo.rowsecurity && tuples > 0)
printTableAddFooter(&cont, _("Policies (Row Security Disabled):"));
diff --git a/src/include/catalog/pg_class.h b/src/include/catalog/pg_class.h
index 25247b5..06d287e 100644
--- a/src/include/catalog/pg_class.h
+++ b/src/include/catalog/pg_class.h
@@ -66,6 +66,7 @@ CATALOG(pg_class,1259) BKI_BOOTSTRAP BKI_ROWTYPE_OID(83) BKI_SCHEMA_MACRO
bool relhastriggers; /* has (or has had) any TRIGGERs */
bool relhassubclass; /* has (or has had) derived classes */
bool relrowsecurity; /* row security is enabled or not */
+ bool relforcerowsecurity; /* row security forced for owners or not */
bool relispopulated; /* matview currently holds query results */
char relreplident; /* see REPLICA_IDENTITY_xxx constants */
TransactionId relfrozenxid; /* all Xids < this are frozen in this rel */
@@ -95,37 +96,38 @@ typedef FormData_pg_class *Form_pg_class;
* ----------------
*/
-#define Natts_pg_class 30
-#define Anum_pg_class_relname 1
-#define Anum_pg_class_relnamespace 2
-#define Anum_pg_class_reltype 3
-#define Anum_pg_class_reloftype 4
-#define Anum_pg_class_relowner 5
-#define Anum_pg_class_relam 6
-#define Anum_pg_class_relfilenode 7
-#define Anum_pg_class_reltablespace 8
-#define Anum_pg_class_relpages 9
-#define Anum_pg_class_reltuples 10
-#define Anum_pg_class_relallvisible 11
-#define Anum_pg_class_reltoastrelid 12
-#define Anum_pg_class_relhasindex 13
-#define Anum_pg_class_relisshared 14
-#define Anum_pg_class_relpersistence 15
-#define Anum_pg_class_relkind 16
-#define Anum_pg_class_relnatts 17
-#define Anum_pg_class_relchecks 18
-#define Anum_pg_class_relhasoids 19
-#define Anum_pg_class_relhaspkey 20
-#define Anum_pg_class_relhasrules 21
-#define Anum_pg_class_relhastriggers 22
-#define Anum_pg_class_relhassubclass 23
-#define Anum_pg_class_relrowsecurity 24
-#define Anum_pg_class_relispopulated 25
-#define Anum_pg_class_relreplident 26
-#define Anum_pg_class_relfrozenxid 27
-#define Anum_pg_class_relminmxid 28
-#define Anum_pg_class_relacl 29
-#define Anum_pg_class_reloptions 30
+#define Natts_pg_class 31
+#define Anum_pg_class_relname 1
+#define Anum_pg_class_relnamespace 2
+#define Anum_pg_class_reltype 3
+#define Anum_pg_class_reloftype 4
+#define Anum_pg_class_relowner 5
+#define Anum_pg_class_relam 6
+#define Anum_pg_class_relfilenode 7
+#define Anum_pg_class_reltablespace 8
+#define Anum_pg_class_relpages 9
+#define Anum_pg_class_reltuples 10
+#define Anum_pg_class_relallvisible 11
+#define Anum_pg_class_reltoastrelid 12
+#define Anum_pg_class_relhasindex 13
+#define Anum_pg_class_relisshared 14
+#define Anum_pg_class_relpersistence 15
+#define Anum_pg_class_relkind 16
+#define Anum_pg_class_relnatts 17
+#define Anum_pg_class_relchecks 18
+#define Anum_pg_class_relhasoids 19
+#define Anum_pg_class_relhaspkey 20
+#define Anum_pg_class_relhasrules 21
+#define Anum_pg_class_relhastriggers 22
+#define Anum_pg_class_relhassubclass 23
+#define Anum_pg_class_relrowsecurity 24
+#define Anum_pg_class_relforcerowsecurity 25
+#define Anum_pg_class_relispopulated 26
+#define Anum_pg_class_relreplident 27
+#define Anum_pg_class_relfrozenxid 28
+#define Anum_pg_class_relminmxid 29
+#define Anum_pg_class_relacl 30
+#define Anum_pg_class_reloptions 31
/* ----------------
* initial contents of pg_class
@@ -140,13 +142,13 @@ typedef FormData_pg_class *Form_pg_class;
* Note: "3" in the relfrozenxid column stands for FirstNormalTransactionId;
* similarly, "1" in relminmxid stands for FirstMultiXactId
*/
-DATA(insert OID = 1247 ( pg_type PGNSP 71 0 PGUID 0 0 0 0 0 0 0 f f p r 30 0 t f f f f f t n 3 1 _null_ _null_ ));
+DATA(insert OID = 1247 ( pg_type PGNSP 71 0 PGUID 0 0 0 0 0 0 0 f f p r 30 0 t f f f f f f t n 3 1 _null_ _null_ ));
DESCR("");
-DATA(insert OID = 1249 ( pg_attribute PGNSP 75 0 PGUID 0 0 0 0 0 0 0 f f p r 21 0 f f f f f f t n 3 1 _null_ _null_ ));
+DATA(insert OID = 1249 ( pg_attribute PGNSP 75 0 PGUID 0 0 0 0 0 0 0 f f p r 21 0 f f f f f f f t n 3 1 _null_ _null_ ));
DESCR("");
-DATA(insert OID = 1255 ( pg_proc PGNSP 81 0 PGUID 0 0 0 0 0 0 0 f f p r 29 0 t f f f f f t n 3 1 _null_ _null_ ));
+DATA(insert OID = 1255 ( pg_proc PGNSP 81 0 PGUID 0 0 0 0 0 0 0 f f p r 29 0 t f f f f f f t n 3 1 _null_ _null_ ));
DESCR("");
-DATA(insert OID = 1259 ( pg_class PGNSP 83 0 PGUID 0 0 0 0 0 0 0 f f p r 30 0 t f f f f f t n 3 1 _null_ _null_ ));
+DATA(insert OID = 1259 ( pg_class PGNSP 83 0 PGUID 0 0 0 0 0 0 0 f f p r 31 0 t f f f f f f t n 3 1 _null_ _null_ ));
DESCR("");
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 770866a..fdf19fa 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -1514,6 +1514,8 @@ typedef enum AlterTableType
AT_ReplicaIdentity, /* REPLICA IDENTITY */
AT_EnableRowSecurity, /* ENABLE ROW SECURITY */
AT_DisableRowSecurity, /* DISABLE ROW SECURITY */
+ AT_ForceRowSecurity, /* FORCE ROW SECURITY */
+ AT_NoForceRowSecurity, /* NO FORCE ROW SECURITY */
AT_GenericOptions /* OPTIONS (...) */
} AlterTableType;
diff --git a/src/test/modules/test_ddl_deparse/test_ddl_deparse.c b/src/test/modules/test_ddl_deparse/test_ddl_deparse.c
index e2dc4b5..4fca737 100644
--- a/src/test/modules/test_ddl_deparse/test_ddl_deparse.c
+++ b/src/test/modules/test_ddl_deparse/test_ddl_deparse.c
@@ -275,6 +275,12 @@ get_altertable_subcmdtypes(PG_FUNCTION_ARGS)
case AT_DisableRowSecurity:
strtype = "DISABLE ROW SECURITY";
break;
+ case AT_ForceRowSecurity:
+ strtype = "FORCE ROW SECURITY";
+ break;
+ case AT_NoForceRowSecurity:
+ strtype = "NO FORCE ROW SECURITY";
+ break;
case AT_GenericOptions:
strtype = "SET OPTIONS";
break;
--
1.9.1
From 2ffb7729679faf5def2d39f224947fa2ca3db364 Mon Sep 17 00:00:00 2001
From: Stephen Frost <sfrost@snowman.net>
Date: Mon, 28 Sep 2015 16:47:42 -0400
Subject: [PATCH 2/2] Add SECURITY_NOFORCE_RLS context
To avoid data corruption when ALTER TABLE .. FORCE ROW SECURITY is
being used, add a SECURITY_NOFORCE_RLS security context and use it
during referential integrity checks. Note that this is only set during
referential integrity checks and is only checked after we have already
checked that the current user is the owner of the relation (which should
always be the case during referential integrity checks).
---
src/backend/utils/adt/ri_triggers.c | 6 ++++--
src/backend/utils/init/miscinit.c | 18 +++++++++++++++++-
src/backend/utils/misc/rls.c | 17 ++++++++++++++++-
src/include/miscadmin.h | 2 ++
4 files changed, 39 insertions(+), 4 deletions(-)
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index 018cb99..6569fdb 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -3014,7 +3014,8 @@ ri_PlanCheck(const char *querystr, int nargs, Oid *argtypes,
/* Switch to proper UID to perform check as */
GetUserIdAndSecContext(&save_userid, &save_sec_context);
SetUserIdAndSecContext(RelationGetForm(query_rel)->relowner,
- save_sec_context | SECURITY_LOCAL_USERID_CHANGE);
+ save_sec_context | SECURITY_LOCAL_USERID_CHANGE |
+ SECURITY_NOFORCE_RLS);
/* Create the plan */
qplan = SPI_prepare(querystr, nargs, argtypes);
@@ -3134,7 +3135,8 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
/* Switch to proper UID to perform check as */
GetUserIdAndSecContext(&save_userid, &save_sec_context);
SetUserIdAndSecContext(RelationGetForm(query_rel)->relowner,
- save_sec_context | SECURITY_LOCAL_USERID_CHANGE);
+ save_sec_context | SECURITY_LOCAL_USERID_CHANGE |
+ SECURITY_NOFORCE_RLS);
/* Finally we can run the query. */
spi_result = SPI_execute_snapshot(qplan,
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index f0099d3..e871fef 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -341,7 +341,7 @@ GetAuthenticatedUserId(void)
* GetUserIdAndSecContext/SetUserIdAndSecContext - get/set the current user ID
* and the SecurityRestrictionContext flags.
*
- * Currently there are two valid bits in SecurityRestrictionContext:
+ * Currently there are three valid bits in SecurityRestrictionContext:
*
* SECURITY_LOCAL_USERID_CHANGE indicates that we are inside an operation
* that is temporarily changing CurrentUserId via these functions. This is
@@ -359,6 +359,13 @@ GetAuthenticatedUserId(void)
* where the called functions are really supposed to be side-effect-free
* anyway, such as VACUUM/ANALYZE/REINDEX.
*
+ * SECURITY_NOFORCE_RLS indicates that we are inside an operation which should
+ * ignore the FORCE ROW LEVEL SECURITY per-table indication. This is used to
+ * ensure that FORCE RLS does not mistakenly break referential integrity
+ * checks. Note that this is intentionally only checked when running as the
+ * owner of the table (which should always be the case for referential
+ * integrity checks).
+ *
* Unlike GetUserId, GetUserIdAndSecContext does *not* Assert that the current
* value of CurrentUserId is valid; nor does SetUserIdAndSecContext require
* the new value to be valid. In fact, these routines had better not
@@ -401,6 +408,15 @@ InSecurityRestrictedOperation(void)
return (SecurityRestrictionContext & SECURITY_RESTRICTED_OPERATION) != 0;
}
+/*
+ * InNoForceRLSOperation - are we ignoring FORCE ROW LEVEL SECURITY ?
+ */
+bool
+InNoForceRLSOperation(void)
+{
+ return (SecurityRestrictionContext & SECURITY_NOFORCE_RLS) != 0;
+}
+
/*
* These are obsolete versions of Get/SetUserIdAndSecContext that are
diff --git a/src/backend/utils/misc/rls.c b/src/backend/utils/misc/rls.c
index b4540ff..e68de47 100644
--- a/src/backend/utils/misc/rls.c
+++ b/src/backend/utils/misc/rls.c
@@ -89,7 +89,22 @@ check_enable_rls(Oid relid, Oid checkAsUser, bool noError)
*/
if (pg_class_ownercheck(relid, user_id))
{
- if (relforcerowsecurity)
+ /*
+ * If FORCE ROW LEVEL SECURITY has been set on the relation then we
+ * return RLS_ENABLED to indicate that RLS should still be applied,
+ * except if we are in a SECURITY_NOFORCE_RLS context, in which case
+ * we return RLS_NONE_ENV.
+ *
+ * The SECURITY_NOFORCE_RLS indicates that we should not apply RLS even
+ * if the table FORCE RLS set- IF the current user is the owner. This
+ * is specifically to ensure that referential integrity checks are
+ * able to still run correctly.
+ *
+ * This is intentionally only done after we have checked that the user
+ * is the table owner, which should always be the case for referential
+ * integrity checks.
+ */
+ if (relforcerowsecurity && !InNoForceRLSOperation())
return RLS_ENABLED;
else
return RLS_NONE_ENV;
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 80ac732..a737dd2 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -286,6 +286,7 @@ extern int trace_recovery(int trace_level);
/* flags to be OR'd to form sec_context */
#define SECURITY_LOCAL_USERID_CHANGE 0x0001
#define SECURITY_RESTRICTED_OPERATION 0x0002
+#define SECURITY_NOFORCE_RLS 0x0004
extern char *DatabasePath;
@@ -304,6 +305,7 @@ extern void GetUserIdAndSecContext(Oid *userid, int *sec_context);
extern void SetUserIdAndSecContext(Oid userid, int sec_context);
extern bool InLocalUserIdChange(void);
extern bool InSecurityRestrictedOperation(void);
+extern bool InNoForceRLSOperation(void);
extern void GetUserIdAndContext(Oid *userid, bool *sec_def_context);
extern void SetUserIdAndContext(Oid userid, bool sec_def_context);
extern void InitializeSessionUserId(const char *rolename, Oid useroid);
--
1.9.1
I've attached a patch to implement it. It's not fully polished but it's
sufficient for comment, I believe. Additional comments, documentation
and regression tests are to be added, if we have agreement on the
grammer and implementation approach.
I have given the first patch a quick review. I think I agree with the
grammar, it makes sense to me. At first I didn't really like NO
FORCE, but I couldn't think of anything better.
One comment:
if (pg_class_ownercheck(relid, user_id))
- return RLS_NONE_ENV;
+ {
+ if (relforcerowsecurity)
+ return RLS_ENABLED;
+ else
+ return RLS_NONE_ENV;
+ }
Is the 'else' even necessary in this block?
Other than that, the approach looks good to me.
The patch applied cleanly against master (758fcfd). As well
'check-world' was successful (obviously understanding that more
related tests are necessary).
I have a hard time with this. We're not talking about the application
logic in this case, we're talking about the guarantees which the
database is making to the user, be it an application or an individual.I've included a patch (the second in the set attached) which adds a
SECURITY_NOFORCE_RLS bit which has a much tighter scope- it only applies
after the regular owner check is done. This reduces the risk of it
being set mistakenly dramatically, I believe. Further, the cascaded
case is correctly handled. This also needs more polish and regression
tests, but I wanted to solicit for comment before moving forward with
that since we don't have a consensus about if this should be done.
I'm not sure that I understand the previous concerns around this bit
well enough to speak intelligently on this point. However, with that
said, I believe the changes made by this patch make sense.
-Adam
--
Adam Brightwell - adam.brightwell@crunchydatasolutions.com
Database Engineer - www.crunchydatasolutions.com
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Mon, Sep 28, 2015 at 05:13:56PM -0400, Stephen Frost wrote:
* Noah Misch (noah@leadboat.com) wrote:
In schema reviews, I will raise a red flag for use of this feature; the best
designs will instead use additional roles. I forecast that PostgreSQL would
fare better with no owner-constrained-by-RLS capability. Even so, others want
it, and FORCE ROW SECURITY would deliver it with an acceptable risk profile.I've attached a patch to implement it. It's not fully polished but it's
sufficient for comment, I believe. Additional comments, documentation
and regression tests are to be added, if we have agreement on the
grammer and implementation approach.
This patch has FORCE ROW LEVEL SECURITY take precedence over row_security=off,
which thwarts pg_dump use of row_security=off to ensure dump completeness.
Should this be a table-level flag, or should it be a policy-level flag? A
policy-level flag is more powerful. If nobody really anticipates using that
power, this table-level flag works for me.
SECURITY_ROW_LEVEL_DISABLED could have been okay. I removed an incomplete
implementation (e.g. didn't affect CASCADE constraints). Writing a full one
would be a mammoth job, and for what? Setting the wrong SELECT policies can
disrupt _any_ application logic; no foreign keys or FORCE ROW SECURITY need be
involved. Protecting just foreign keys brings some value, but it will not
materially reduce the vigilance demanded of RLS policy authors and reviewers.I have a hard time with this. We're not talking about the application
logic in this case, we're talking about the guarantees which the
database is making to the user, be it an application or an individual.
If disabling policies has an effect, table owners must be feeding conflicting
requirements into the system. Violating policies during referential integrity
queries is not, in general, less serious than violating referential integrity
itself. Rules and triggers pose the same threat, and we let those break
referential integrity. I think the ideal in all of these cases is rather to
detect the conflict and raise an error. SECURITY_ROW_LEVEL_DISABLED and
SECURITY_NOFORCE_RLS send policies in a third direction, neither the beaten
path of rules/triggers nor the ideal.
I've included a patch (the second in the set attached) which adds a
SECURITY_NOFORCE_RLS bit which has a much tighter scope- it only applies
after the regular owner check is done. This reduces the risk of it
being set mistakenly dramatically, I believe.
Yes, that's far safer than SECURITY_ROW_LEVEL_DISABLED. I assume the final
design will let table owners completely bypass FORCE ROW LEVEL SECURITY under
"SET row_security = off". If so, SECURITY_NOFORCE_RLS poses negligible risk.
Even if not, SECURITY_NOFORCE_RLS poses low risk.
@@ -3667,6 +3673,12 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab, Relation rel, case AT_DisableRowSecurity: ATExecDisableRowSecurity(rel); break; + case AT_ForceRowSecurity: + ATExecForceRowSecurity(rel); + break; + case AT_NoForceRowSecurity: + ATExecNoForceRowSecurity(rel); + break;
Functions differing only in s/ = true/ = false/? ATExecEnableDisableTrigger()
is a better model for this.
nm
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Thu, Oct 1, 2015 at 11:10 PM, Noah Misch <noah@leadboat.com> wrote:
On Mon, Sep 28, 2015 at 05:13:56PM -0400, Stephen Frost wrote:
* Noah Misch (noah@leadboat.com) wrote:
In schema reviews, I will raise a red flag for use of this feature; the best
designs will instead use additional roles. I forecast that PostgreSQL would
fare better with no owner-constrained-by-RLS capability. Even so, others want
it, and FORCE ROW SECURITY would deliver it with an acceptable risk profile.I've attached a patch to implement it. It's not fully polished but it's
sufficient for comment, I believe. Additional comments, documentation
and regression tests are to be added, if we have agreement on the
grammer and implementation approach.This patch has FORCE ROW LEVEL SECURITY take precedence over row_security=off,
which thwarts pg_dump use of row_security=off to ensure dump completeness.
Yeah, I think that's NOT ok.
Should this be a table-level flag, or should it be a policy-level flag? A
policy-level flag is more powerful. If nobody really anticipates using that
power, this table-level flag works for me.
Either works for me.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
* Noah Misch (noah@leadboat.com) wrote:
On Mon, Sep 28, 2015 at 05:13:56PM -0400, Stephen Frost wrote:
* Noah Misch (noah@leadboat.com) wrote:
In schema reviews, I will raise a red flag for use of this feature; the best
designs will instead use additional roles. I forecast that PostgreSQL would
fare better with no owner-constrained-by-RLS capability. Even so, others want
it, and FORCE ROW SECURITY would deliver it with an acceptable risk profile.I've attached a patch to implement it. It's not fully polished but it's
sufficient for comment, I believe. Additional comments, documentation
and regression tests are to be added, if we have agreement on the
grammer and implementation approach.This patch has FORCE ROW LEVEL SECURITY take precedence over row_security=off,
which thwarts pg_dump use of row_security=off to ensure dump completeness.
Fixed.
Should this be a table-level flag, or should it be a policy-level flag? A
policy-level flag is more powerful. If nobody really anticipates using that
power, this table-level flag works for me.
table-level seems the right level to me and no one is calling for
policy-level. Further, policy-level could be added later if there ends
up being significant interest later.
SECURITY_ROW_LEVEL_DISABLED could have been okay. I removed an incomplete
implementation (e.g. didn't affect CASCADE constraints). Writing a full one
would be a mammoth job, and for what? Setting the wrong SELECT policies can
disrupt _any_ application logic; no foreign keys or FORCE ROW SECURITY need be
involved. Protecting just foreign keys brings some value, but it will not
materially reduce the vigilance demanded of RLS policy authors and reviewers.I have a hard time with this. We're not talking about the application
logic in this case, we're talking about the guarantees which the
database is making to the user, be it an application or an individual.If disabling policies has an effect, table owners must be feeding conflicting
requirements into the system. Violating policies during referential integrity
queries is not, in general, less serious than violating referential integrity
itself. Rules and triggers pose the same threat, and we let those break
referential integrity. I think the ideal in all of these cases is rather to
detect the conflict and raise an error. SECURITY_ROW_LEVEL_DISABLED and
SECURITY_NOFORCE_RLS send policies in a third direction, neither the beaten
path of rules/triggers nor the ideal.
The agreement between the user and the system with regard to permissions
and referential integrity is that referential integrity takes priority
over permissions. Prior to FORCE and SECURITY_NOFORCE_RLS that is true
for RLS. I don't believe it makes sense that adding FORCE would change
that agreement, nor do I agree that the ideal would be for the system to
throw errors when the permissions system would deny access during RI
checks.
While I appreciate that rules and triggers can break RI, the way RLS
works is consistent with the ACL system and FORCE should be consistent
with the ACL system and normal/non-FORCE RLS with regard to referential
integrity.
I've included a patch (the second in the set attached) which adds a
SECURITY_NOFORCE_RLS bit which has a much tighter scope- it only applies
after the regular owner check is done. This reduces the risk of it
being set mistakenly dramatically, I believe.Yes, that's far safer than SECURITY_ROW_LEVEL_DISABLED. I assume the final
design will let table owners completely bypass FORCE ROW LEVEL SECURITY under
"SET row_security = off". If so, SECURITY_NOFORCE_RLS poses negligible risk.
I've made that change.
Functions differing only in s/ = true/ = false/? ATExecEnableDisableTrigger()
is a better model for this.
I've changed that to be one function. As an independent patch, I'll do
the same for ATExecEnable/DisableRowSecurity.
Apologies about the timing, I had intended to get this done yesterday.
Barring further concerns, I'll push the attached later today with the
necessary catversion bump.
Thanks!
Stephen
Attachments:
rls-force.v2.patchtext/x-diff; charset=us-asciiDownload
From 810c8f6ea717303a537b1c80337f98d3ad282645 Mon Sep 17 00:00:00 2001
From: Stephen Frost <sfrost@snowman.net>
Date: Mon, 28 Sep 2015 11:28:15 -0400
Subject: [PATCH] ALTER TABLE .. FORCE ROW LEVEL SECURITY
To allow users to force RLS to always be applied, even for table owners,
add ALTER TABLE .. FORCE ROW LEVEL SECURITY.
row_security=off overrides FORCE ROW LEVEL SECURITY, to ensure pg_dump
output is complete (by default).
Also add SECURITY_NOFORCE_RLS context to avoid data corruption when
ALTER TABLE .. FORCE ROW SECURITY is being used. The
SECURITY_NOFORCE_RLS security context is used only during referential
integrity checks and is only considered in check_enable_rls() after we
have already checked that the current user is the owner of the relation
(which should always be the case during referential integrity checks).
Back-patch to 9.5 where RLS was added.
---
doc/src/sgml/catalogs.sgml | 10 ++
doc/src/sgml/ref/alter_table.sgml | 17 +++
src/backend/catalog/heap.c | 1 +
src/backend/commands/tablecmds.c | 40 ++++++
src/backend/parser/gram.y | 14 ++
src/backend/utils/adt/ri_triggers.c | 6 +-
src/backend/utils/init/miscinit.c | 18 ++-
src/backend/utils/misc/rls.c | 44 +++++-
src/bin/pg_dump/pg_dump.c | 20 ++-
src/bin/pg_dump/pg_dump.h | 1 +
src/bin/psql/describe.c | 44 +++---
src/include/catalog/pg_class.h | 72 +++++-----
src/include/miscadmin.h | 2 +
src/include/nodes/parsenodes.h | 2 +
.../modules/test_ddl_deparse/test_ddl_deparse.c | 6 +
src/test/regress/expected/rowsecurity.out | 156 +++++++++++++++++++++
src/test/regress/output/misc.source | 3 +-
src/test/regress/sql/rowsecurity.sql | 143 +++++++++++++++++++
18 files changed, 536 insertions(+), 63 deletions(-)
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 28bb480..97ef618 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -1972,6 +1972,16 @@
</row>
<row>
+ <entry><structfield>relforcerowsecurity</structfield></entry>
+ <entry><type>bool</type></entry>
+ <entry></entry>
+ <entry>
+ True if row level security (when enabled) will also apply to table owner; see
+ <link linkend="catalog-pg-policy"><structname>pg_policy</structname></link> catalog
+ </entry>
+ </row>
+
+ <row>
<entry><structfield>relispopulated</structfield></entry>
<entry><type>bool</type></entry>
<entry></entry>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 8e35cd9..aca40f5 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -61,6 +61,8 @@ ALTER TABLE ALL IN TABLESPACE <replaceable class="PARAMETER">name</replaceable>
ENABLE ALWAYS RULE <replaceable class="PARAMETER">rewrite_rule_name</replaceable>
DISABLE ROW LEVEL SECURITY
ENABLE ROW LEVEL SECURITY
+ FORCE ROW LEVEL SECURITY
+ NO FORCE ROW LEVEL SECURITY
CLUSTER ON <replaceable class="PARAMETER">index_name</replaceable>
SET WITHOUT CLUSTER
SET WITH OIDS
@@ -434,6 +436,21 @@ ALTER TABLE ALL IN TABLESPACE <replaceable class="PARAMETER">name</replaceable>
</varlistentry>
<varlistentry>
+ <term><literal>NO FORCE</literal>/<literal>FORCE ROW LEVEL SECURITY</literal></term>
+ <listitem>
+ <para>
+ These forms control the application of row security policies belonging
+ to the table when the user is the table owner. If enabled, row level
+ security policies will be applied when the user is the table owner. If
+ disabled (the default) then row level security will not be applied when
+ the user is the table owner.
+ See also
+ <xref linkend="SQL-CREATEPOLICY">.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
<term><literal>CLUSTER ON</literal></term>
<listitem>
<para>
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index d04e94d..7d7d062 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -802,6 +802,7 @@ InsertPgClassTuple(Relation pg_class_desc,
values[Anum_pg_class_relhasrules - 1] = BoolGetDatum(rd_rel->relhasrules);
values[Anum_pg_class_relhastriggers - 1] = BoolGetDatum(rd_rel->relhastriggers);
values[Anum_pg_class_relrowsecurity - 1] = BoolGetDatum(rd_rel->relrowsecurity);
+ values[Anum_pg_class_relforcerowsecurity - 1] = BoolGetDatum(rd_rel->relforcerowsecurity);
values[Anum_pg_class_relhassubclass - 1] = BoolGetDatum(rd_rel->relhassubclass);
values[Anum_pg_class_relispopulated - 1] = BoolGetDatum(rd_rel->relispopulated);
values[Anum_pg_class_relreplident - 1] = CharGetDatum(rd_rel->relreplident);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 126b119..7668c9d 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -419,6 +419,7 @@ static void ATExecReplicaIdentity(Relation rel, ReplicaIdentityStmt *stmt, LOCKM
static void ATExecGenericOptions(Relation rel, List *options);
static void ATExecEnableRowSecurity(Relation rel);
static void ATExecDisableRowSecurity(Relation rel);
+static void ATExecForceNoForceRowSecurity(Relation rel, bool force_rls);
static void copy_relation_data(SMgrRelation rel, SMgrRelation dst,
ForkNumber forkNum, char relpersistence);
@@ -2930,6 +2931,8 @@ AlterTableGetLockLevel(List *cmds)
case AT_SetNotNull:
case AT_EnableRowSecurity:
case AT_DisableRowSecurity:
+ case AT_ForceRowSecurity:
+ case AT_NoForceRowSecurity:
cmd_lockmode = AccessExclusiveLock;
break;
@@ -3351,6 +3354,8 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
case AT_DropOf: /* NOT OF */
case AT_EnableRowSecurity:
case AT_DisableRowSecurity:
+ case AT_ForceRowSecurity:
+ case AT_NoForceRowSecurity:
ATSimplePermissions(rel, ATT_TABLE);
/* These commands never recurse */
/* No command-specific prep needed */
@@ -3667,6 +3672,12 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab, Relation rel,
case AT_DisableRowSecurity:
ATExecDisableRowSecurity(rel);
break;
+ case AT_ForceRowSecurity:
+ ATExecForceNoForceRowSecurity(rel, true);
+ break;
+ case AT_NoForceRowSecurity:
+ ATExecForceNoForceRowSecurity(rel, false);
+ break;
case AT_GenericOptions:
ATExecGenericOptions(rel, (List *) cmd->def);
break;
@@ -11067,6 +11078,35 @@ ATExecDisableRowSecurity(Relation rel)
}
/*
+ * ALTER TABLE FORCE/NO FORCE ROW LEVEL SECURITY
+ */
+static void
+ATExecForceNoForceRowSecurity(Relation rel, bool force_rls)
+{
+ Relation pg_class;
+ Oid relid;
+ HeapTuple tuple;
+
+ relid = RelationGetRelid(rel);
+
+ pg_class = heap_open(RelationRelationId, RowExclusiveLock);
+
+ tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
+
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for relation %u", relid);
+
+ ((Form_pg_class) GETSTRUCT(tuple))->relforcerowsecurity = force_rls;
+ simple_heap_update(pg_class, &tuple->t_self, tuple);
+
+ /* keep catalog indexes current */
+ CatalogUpdateIndexes(pg_class, tuple);
+
+ heap_close(pg_class, RowExclusiveLock);
+ heap_freetuple(tuple);
+}
+
+/*
* ALTER FOREIGN TABLE <name> OPTIONS (...)
*/
static void
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 417fb55..8bd5119 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -2353,6 +2353,20 @@ alter_table_cmd:
n->subtype = AT_DisableRowSecurity;
$$ = (Node *)n;
}
+ /* ALTER TABLE <name> FORCE ROW LEVEL SECURITY */
+ | FORCE ROW LEVEL SECURITY
+ {
+ AlterTableCmd *n = makeNode(AlterTableCmd);
+ n->subtype = AT_ForceRowSecurity;
+ $$ = (Node *)n;
+ }
+ /* ALTER TABLE <name> NO FORCE ROW LEVEL SECURITY */
+ | NO FORCE ROW LEVEL SECURITY
+ {
+ AlterTableCmd *n = makeNode(AlterTableCmd);
+ n->subtype = AT_NoForceRowSecurity;
+ $$ = (Node *)n;
+ }
| alter_generic_options
{
AlterTableCmd *n = makeNode(AlterTableCmd);
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index 018cb99..6569fdb 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -3014,7 +3014,8 @@ ri_PlanCheck(const char *querystr, int nargs, Oid *argtypes,
/* Switch to proper UID to perform check as */
GetUserIdAndSecContext(&save_userid, &save_sec_context);
SetUserIdAndSecContext(RelationGetForm(query_rel)->relowner,
- save_sec_context | SECURITY_LOCAL_USERID_CHANGE);
+ save_sec_context | SECURITY_LOCAL_USERID_CHANGE |
+ SECURITY_NOFORCE_RLS);
/* Create the plan */
qplan = SPI_prepare(querystr, nargs, argtypes);
@@ -3134,7 +3135,8 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
/* Switch to proper UID to perform check as */
GetUserIdAndSecContext(&save_userid, &save_sec_context);
SetUserIdAndSecContext(RelationGetForm(query_rel)->relowner,
- save_sec_context | SECURITY_LOCAL_USERID_CHANGE);
+ save_sec_context | SECURITY_LOCAL_USERID_CHANGE |
+ SECURITY_NOFORCE_RLS);
/* Finally we can run the query. */
spi_result = SPI_execute_snapshot(qplan,
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index f0099d3..e871fef 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -341,7 +341,7 @@ GetAuthenticatedUserId(void)
* GetUserIdAndSecContext/SetUserIdAndSecContext - get/set the current user ID
* and the SecurityRestrictionContext flags.
*
- * Currently there are two valid bits in SecurityRestrictionContext:
+ * Currently there are three valid bits in SecurityRestrictionContext:
*
* SECURITY_LOCAL_USERID_CHANGE indicates that we are inside an operation
* that is temporarily changing CurrentUserId via these functions. This is
@@ -359,6 +359,13 @@ GetAuthenticatedUserId(void)
* where the called functions are really supposed to be side-effect-free
* anyway, such as VACUUM/ANALYZE/REINDEX.
*
+ * SECURITY_NOFORCE_RLS indicates that we are inside an operation which should
+ * ignore the FORCE ROW LEVEL SECURITY per-table indication. This is used to
+ * ensure that FORCE RLS does not mistakenly break referential integrity
+ * checks. Note that this is intentionally only checked when running as the
+ * owner of the table (which should always be the case for referential
+ * integrity checks).
+ *
* Unlike GetUserId, GetUserIdAndSecContext does *not* Assert that the current
* value of CurrentUserId is valid; nor does SetUserIdAndSecContext require
* the new value to be valid. In fact, these routines had better not
@@ -401,6 +408,15 @@ InSecurityRestrictedOperation(void)
return (SecurityRestrictionContext & SECURITY_RESTRICTED_OPERATION) != 0;
}
+/*
+ * InNoForceRLSOperation - are we ignoring FORCE ROW LEVEL SECURITY ?
+ */
+bool
+InNoForceRLSOperation(void)
+{
+ return (SecurityRestrictionContext & SECURITY_NOFORCE_RLS) != 0;
+}
+
/*
* These are obsolete versions of Get/SetUserIdAndSecContext that are
diff --git a/src/backend/utils/misc/rls.c b/src/backend/utils/misc/rls.c
index eaf9d6e..6ce92af 100644
--- a/src/backend/utils/misc/rls.c
+++ b/src/backend/utils/misc/rls.c
@@ -55,6 +55,7 @@ check_enable_rls(Oid relid, Oid checkAsUser, bool noError)
HeapTuple tuple;
Form_pg_class classform;
bool relrowsecurity;
+ bool relforcerowsecurity;
Oid user_id = checkAsUser ? checkAsUser : GetUserId();
/* Nothing to do for built-in relations */
@@ -68,6 +69,7 @@ check_enable_rls(Oid relid, Oid checkAsUser, bool noError)
classform = (Form_pg_class) GETSTRUCT(tuple);
relrowsecurity = classform->relrowsecurity;
+ relforcerowsecurity = classform->relforcerowsecurity;
ReleaseSysCache(tuple);
@@ -76,14 +78,46 @@ check_enable_rls(Oid relid, Oid checkAsUser, bool noError)
return RLS_NONE;
/*
- * Table owners and BYPASSRLS users bypass RLS. Note that a superuser
- * qualifies as both. Return RLS_NONE_ENV to indicate that this decision
- * depends on the environment (in this case, the user_id).
+ * BYPASSRLS users always bypass RLS. Note that superusers are always
+ * considered to have BYPASSRLS.
+ *
+ * Return RLS_NONE_ENV to indicate that this decision depends on the
+ * environment (in this case, the user_id).
*/
- if (pg_class_ownercheck(relid, user_id) ||
- has_bypassrls_privilege(user_id))
+ if (has_bypassrls_privilege(user_id))
return RLS_NONE_ENV;
+ /*
+ * Table owners generally bypass RLS, except if row_security=true and the
+ * table has been set (by an owner) to FORCE ROW SECURITY, and this is not
+ * a referential integrity check.
+ *
+ * Return RLS_NONE_ENV to indicate that this decision depends on the
+ * environment (in this case, the user_id).
+ */
+ if (pg_class_ownercheck(relid, user_id))
+ {
+ /*
+ * If row_security=true and FORCE ROW LEVEL SECURITY has been set on
+ * the relation then we return RLS_ENABLED to indicate that RLS should
+ * still be applied. If we are in a SECURITY_NOFORCE_RLS context or if
+ * row_security=false then we return RLS_NONE_ENV.
+ *
+ * The SECURITY_NOFORCE_RLS indicates that we should not apply RLS even
+ * if the table has FORCE RLS set- IF the current user is the owner.
+ * This is specifically to ensure that referential integrity checks are
+ * able to still run correctly.
+ *
+ * This is intentionally only done after we have checked that the user
+ * is the table owner, which should always be the case for referential
+ * integrity checks.
+ */
+ if (row_security && relforcerowsecurity && !InNoForceRLSOperation())
+ return RLS_ENABLED;
+ else
+ return RLS_NONE_ENV;
+ }
+
/* row_security GUC says to bypass RLS, but user lacks permission */
if (!row_security && !noError)
ereport(ERROR,
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index ba1683b..36863df 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -4540,6 +4540,7 @@ getTables(Archive *fout, DumpOptions *dopt, int *numTables)
int i_relhasindex;
int i_relhasrules;
int i_relrowsec;
+ int i_relforcerowsec;
int i_relhasoids;
int i_relfrozenxid;
int i_relminmxid;
@@ -4593,7 +4594,7 @@ getTables(Archive *fout, DumpOptions *dopt, int *numTables)
"(%s c.relowner) AS rolname, "
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
- "c.relrowsecurity, "
+ "c.relrowsecurity, c.relforcerowsecurity, "
"c.relfrozenxid, c.relminmxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"tc.relminmxid AS tminmxid, "
@@ -4635,6 +4636,7 @@ getTables(Archive *fout, DumpOptions *dopt, int *numTables)
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
"'f'::bool AS relrowsecurity, "
+ "'f'::bool AS relforcerowsecurity, "
"c.relfrozenxid, c.relminmxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"tc.relminmxid AS tminmxid, "
@@ -4676,6 +4678,7 @@ getTables(Archive *fout, DumpOptions *dopt, int *numTables)
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
"'f'::bool AS relrowsecurity, "
+ "'f'::bool AS relforcerowsecurity, "
"c.relfrozenxid, c.relminmxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"tc.relminmxid AS tminmxid, "
@@ -4717,6 +4720,7 @@ getTables(Archive *fout, DumpOptions *dopt, int *numTables)
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
"'f'::bool AS relrowsecurity, "
+ "'f'::bool AS relforcerowsecurity, "
"c.relfrozenxid, 0 AS relminmxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"0 AS tminmxid, "
@@ -4756,6 +4760,7 @@ getTables(Archive *fout, DumpOptions *dopt, int *numTables)
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
"'f'::bool AS relrowsecurity, "
+ "'f'::bool AS relforcerowsecurity, "
"c.relfrozenxid, 0 AS relminmxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"0 AS tminmxid, "
@@ -4794,6 +4799,7 @@ getTables(Archive *fout, DumpOptions *dopt, int *numTables)
"c.relchecks, c.relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
"'f'::bool AS relrowsecurity, "
+ "'f'::bool AS relforcerowsecurity, "
"c.relfrozenxid, 0 AS relminmxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"0 AS tminmxid, "
@@ -4832,6 +4838,7 @@ getTables(Archive *fout, DumpOptions *dopt, int *numTables)
"c.relchecks, (c.reltriggers <> 0) AS relhastriggers, "
"c.relhasindex, c.relhasrules, c.relhasoids, "
"'f'::bool AS relrowsecurity, "
+ "'f'::bool AS relforcerowsecurity, "
"c.relfrozenxid, 0 AS relminmxid, tc.oid AS toid, "
"tc.relfrozenxid AS tfrozenxid, "
"0 AS tminmxid, "
@@ -4870,6 +4877,7 @@ getTables(Archive *fout, DumpOptions *dopt, int *numTables)
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, relhasoids, "
"'f'::bool AS relrowsecurity, "
+ "'f'::bool AS relforcerowsecurity, "
"0 AS relfrozenxid, 0 AS relminmxid,"
"0 AS toid, "
"0 AS tfrozenxid, 0 AS tminmxid,"
@@ -4907,6 +4915,7 @@ getTables(Archive *fout, DumpOptions *dopt, int *numTables)
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, relhasoids, "
"'f'::bool AS relrowsecurity, "
+ "'f'::bool AS relforcerowsecurity, "
"0 AS relfrozenxid, 0 AS relminmxid,"
"0 AS toid, "
"0 AS tfrozenxid, 0 AS tminmxid,"
@@ -4940,6 +4949,7 @@ getTables(Archive *fout, DumpOptions *dopt, int *numTables)
"relchecks, (reltriggers <> 0) AS relhastriggers, "
"relhasindex, relhasrules, relhasoids, "
"'f'::bool AS relrowsecurity, "
+ "'f'::bool AS relforcerowsecurity, "
"0 AS relfrozenxid, 0 AS relminmxid,"
"0 AS toid, "
"0 AS tfrozenxid, 0 AS tminmxid,"
@@ -4968,6 +4978,7 @@ getTables(Archive *fout, DumpOptions *dopt, int *numTables)
"relhasindex, relhasrules, "
"'t'::bool AS relhasoids, "
"'f'::bool AS relrowsecurity, "
+ "'f'::bool AS relforcerowsecurity, "
"0 AS relfrozenxid, 0 AS relminmxid,"
"0 AS toid, "
"0 AS tfrozenxid, 0 AS tminmxid,"
@@ -5006,6 +5017,7 @@ getTables(Archive *fout, DumpOptions *dopt, int *numTables)
"relhasindex, relhasrules, "
"'t'::bool AS relhasoids, "
"'f'::bool AS relrowsecurity, "
+ "'f'::bool AS relforcerowsecurity, "
"0 AS relfrozenxid, 0 AS relminmxid,"
"0 AS toid, "
"0 AS tfrozenxid, 0 AS tminmxid,"
@@ -5054,6 +5066,7 @@ getTables(Archive *fout, DumpOptions *dopt, int *numTables)
i_relhasindex = PQfnumber(res, "relhasindex");
i_relhasrules = PQfnumber(res, "relhasrules");
i_relrowsec = PQfnumber(res, "relrowsecurity");
+ i_relforcerowsec = PQfnumber(res, "relforcerowsecurity");
i_relhasoids = PQfnumber(res, "relhasoids");
i_relfrozenxid = PQfnumber(res, "relfrozenxid");
i_relminmxid = PQfnumber(res, "relminmxid");
@@ -5106,6 +5119,7 @@ getTables(Archive *fout, DumpOptions *dopt, int *numTables)
tblinfo[i].hasrules = (strcmp(PQgetvalue(res, i, i_relhasrules), "t") == 0);
tblinfo[i].hastriggers = (strcmp(PQgetvalue(res, i, i_relhastriggers), "t") == 0);
tblinfo[i].rowsec = (strcmp(PQgetvalue(res, i, i_relrowsec), "t") == 0);
+ tblinfo[i].forcerowsec = (strcmp(PQgetvalue(res, i, i_relforcerowsec), "t") == 0);
tblinfo[i].hasoids = (strcmp(PQgetvalue(res, i, i_relhasoids), "t") == 0);
tblinfo[i].relispopulated = (strcmp(PQgetvalue(res, i, i_relispopulated), "t") == 0);
tblinfo[i].relreplident = *(PQgetvalue(res, i, i_relreplident));
@@ -14412,6 +14426,10 @@ dumpTableSchema(Archive *fout, DumpOptions *dopt, TableInfo *tbinfo)
appendPQExpBuffer(q, "\nALTER TABLE ONLY %s SET WITH OIDS;\n",
fmtId(tbinfo->dobj.name));
+ if (tbinfo->forcerowsec)
+ appendPQExpBuffer(q, "\nALTER TABLE ONLY %s FORCE ROW LEVEL SECURITY;\n",
+ fmtId(tbinfo->dobj.name));
+
if (dopt->binary_upgrade)
binary_upgrade_extension_member(q, &tbinfo->dobj, labelq->data);
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index b40b816..3c64a82 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -211,6 +211,7 @@ typedef struct _tableInfo
bool hasrules; /* does it have any rules? */
bool hastriggers; /* does it have any triggers? */
bool rowsec; /* is row security enabled? */
+ bool forcerowsec; /* is row security forced? */
bool hasoids; /* does it have OIDs? */
uint32 frozenxid; /* for restore frozen xid */
uint32 minmxid; /* for restore min multi xid */
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 898f8b3..92ed6e2 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -1234,6 +1234,7 @@ describeOneTableDetails(const char *schemaname,
bool hasrules;
bool hastriggers;
bool rowsecurity;
+ bool forcerowsecurity;
bool hasoids;
Oid tablespace;
char *reloptions;
@@ -1259,8 +1260,8 @@ describeOneTableDetails(const char *schemaname,
{
printfPQExpBuffer(&buf,
"SELECT c.relchecks, c.relkind, c.relhasindex, c.relhasrules, "
- "c.relhastriggers, c.relrowsecurity, c.relhasoids, "
- "%s, c.reltablespace, "
+ "c.relhastriggers, c.relrowsecurity, c.relforcerowsecurity, "
+ "c.relhasoids, %s, c.reltablespace, "
"CASE WHEN c.reloftype = 0 THEN '' ELSE c.reloftype::pg_catalog.regtype::pg_catalog.text END, "
"c.relpersistence, c.relreplident\n"
"FROM pg_catalog.pg_class c\n "
@@ -1276,7 +1277,7 @@ describeOneTableDetails(const char *schemaname,
{
printfPQExpBuffer(&buf,
"SELECT c.relchecks, c.relkind, c.relhasindex, c.relhasrules, "
- "c.relhastriggers, false, c.relhasoids, "
+ "c.relhastriggers, false, false, c.relhasoids, "
"%s, c.reltablespace, "
"CASE WHEN c.reloftype = 0 THEN '' ELSE c.reloftype::pg_catalog.regtype::pg_catalog.text END, "
"c.relpersistence, c.relreplident\n"
@@ -1293,7 +1294,7 @@ describeOneTableDetails(const char *schemaname,
{
printfPQExpBuffer(&buf,
"SELECT c.relchecks, c.relkind, c.relhasindex, c.relhasrules, "
- "c.relhastriggers, false, c.relhasoids, "
+ "c.relhastriggers, false, false, c.relhasoids, "
"%s, c.reltablespace, "
"CASE WHEN c.reloftype = 0 THEN '' ELSE c.reloftype::pg_catalog.regtype::pg_catalog.text END, "
"c.relpersistence\n"
@@ -1310,7 +1311,7 @@ describeOneTableDetails(const char *schemaname,
{
printfPQExpBuffer(&buf,
"SELECT c.relchecks, c.relkind, c.relhasindex, c.relhasrules, "
- "c.relhastriggers, false, c.relhasoids, "
+ "c.relhastriggers, false, false, c.relhasoids, "
"%s, c.reltablespace, "
"CASE WHEN c.reloftype = 0 THEN '' ELSE c.reloftype::pg_catalog.regtype::pg_catalog.text END\n"
"FROM pg_catalog.pg_class c\n "
@@ -1326,7 +1327,7 @@ describeOneTableDetails(const char *schemaname,
{
printfPQExpBuffer(&buf,
"SELECT c.relchecks, c.relkind, c.relhasindex, c.relhasrules, "
- "c.relhastriggers, false, c.relhasoids, "
+ "c.relhastriggers, false, false, c.relhasoids, "
"%s, c.reltablespace\n"
"FROM pg_catalog.pg_class c\n "
"LEFT JOIN pg_catalog.pg_class tc ON (c.reltoastrelid = tc.oid)\n"
@@ -1341,7 +1342,7 @@ describeOneTableDetails(const char *schemaname,
{
printfPQExpBuffer(&buf,
"SELECT relchecks, relkind, relhasindex, relhasrules, "
- "reltriggers <> 0, false, relhasoids, "
+ "reltriggers <> 0, false, false, relhasoids, "
"%s, reltablespace\n"
"FROM pg_catalog.pg_class WHERE oid = '%s';",
(verbose ?
@@ -1352,7 +1353,7 @@ describeOneTableDetails(const char *schemaname,
{
printfPQExpBuffer(&buf,
"SELECT relchecks, relkind, relhasindex, relhasrules, "
- "reltriggers <> 0, false, relhasoids, "
+ "reltriggers <> 0, false, false, relhasoids, "
"'', reltablespace\n"
"FROM pg_catalog.pg_class WHERE oid = '%s';",
oid);
@@ -1361,7 +1362,7 @@ describeOneTableDetails(const char *schemaname,
{
printfPQExpBuffer(&buf,
"SELECT relchecks, relkind, relhasindex, relhasrules, "
- "reltriggers <> 0, false, relhasoids, "
+ "reltriggers <> 0, false, false, relhasoids, "
"'', ''\n"
"FROM pg_catalog.pg_class WHERE oid = '%s';",
oid);
@@ -1385,18 +1386,19 @@ describeOneTableDetails(const char *schemaname,
tableinfo.hasrules = strcmp(PQgetvalue(res, 0, 3), "t") == 0;
tableinfo.hastriggers = strcmp(PQgetvalue(res, 0, 4), "t") == 0;
tableinfo.rowsecurity = strcmp(PQgetvalue(res, 0, 5), "t") == 0;
- tableinfo.hasoids = strcmp(PQgetvalue(res, 0, 6), "t") == 0;
+ tableinfo.forcerowsecurity = strcmp(PQgetvalue(res, 0, 6), "t") == 0;
+ tableinfo.hasoids = strcmp(PQgetvalue(res, 0, 7), "t") == 0;
tableinfo.reloptions = (pset.sversion >= 80200) ?
- pg_strdup(PQgetvalue(res, 0, 7)) : NULL;
+ pg_strdup(PQgetvalue(res, 0, 8)) : NULL;
tableinfo.tablespace = (pset.sversion >= 80000) ?
- atooid(PQgetvalue(res, 0, 8)) : 0;
+ atooid(PQgetvalue(res, 0, 9)) : 0;
tableinfo.reloftype = (pset.sversion >= 90000 &&
- strcmp(PQgetvalue(res, 0, 9), "") != 0) ?
- pg_strdup(PQgetvalue(res, 0, 9)) : NULL;
+ strcmp(PQgetvalue(res, 0, 10), "") != 0) ?
+ pg_strdup(PQgetvalue(res, 0, 10)) : NULL;
tableinfo.relpersistence = (pset.sversion >= 90100) ?
- *(PQgetvalue(res, 0, 10)) : 0;
+ *(PQgetvalue(res, 0, 11)) : 0;
tableinfo.relreplident = (pset.sversion >= 90400) ?
- *(PQgetvalue(res, 0, 11)) : 'd';
+ *(PQgetvalue(res, 0, 12)) : 'd';
PQclear(res);
res = NULL;
@@ -2057,12 +2059,18 @@ describeOneTableDetails(const char *schemaname,
* there aren't policies, or RLS isn't enabled but there are
* policies
*/
- if (tableinfo.rowsecurity && tuples > 0)
+ if (tableinfo.rowsecurity && !tableinfo.forcerowsecurity && tuples > 0)
printTableAddFooter(&cont, _("Policies:"));
- if (tableinfo.rowsecurity && tuples == 0)
+ if (tableinfo.rowsecurity && tableinfo.forcerowsecurity && tuples > 0)
+ printTableAddFooter(&cont, _("Policies (Forced Row Security Enabled):"));
+
+ if (tableinfo.rowsecurity && !tableinfo.forcerowsecurity && tuples == 0)
printTableAddFooter(&cont, _("Policies (Row Security Enabled): (None)"));
+ if (tableinfo.rowsecurity && tableinfo.forcerowsecurity && tuples == 0)
+ printTableAddFooter(&cont, _("Policies (Forced Row Security Enabled): (None)"));
+
if (!tableinfo.rowsecurity && tuples > 0)
printTableAddFooter(&cont, _("Policies (Row Security Disabled):"));
diff --git a/src/include/catalog/pg_class.h b/src/include/catalog/pg_class.h
index 25247b5..06d287e 100644
--- a/src/include/catalog/pg_class.h
+++ b/src/include/catalog/pg_class.h
@@ -66,6 +66,7 @@ CATALOG(pg_class,1259) BKI_BOOTSTRAP BKI_ROWTYPE_OID(83) BKI_SCHEMA_MACRO
bool relhastriggers; /* has (or has had) any TRIGGERs */
bool relhassubclass; /* has (or has had) derived classes */
bool relrowsecurity; /* row security is enabled or not */
+ bool relforcerowsecurity; /* row security forced for owners or not */
bool relispopulated; /* matview currently holds query results */
char relreplident; /* see REPLICA_IDENTITY_xxx constants */
TransactionId relfrozenxid; /* all Xids < this are frozen in this rel */
@@ -95,37 +96,38 @@ typedef FormData_pg_class *Form_pg_class;
* ----------------
*/
-#define Natts_pg_class 30
-#define Anum_pg_class_relname 1
-#define Anum_pg_class_relnamespace 2
-#define Anum_pg_class_reltype 3
-#define Anum_pg_class_reloftype 4
-#define Anum_pg_class_relowner 5
-#define Anum_pg_class_relam 6
-#define Anum_pg_class_relfilenode 7
-#define Anum_pg_class_reltablespace 8
-#define Anum_pg_class_relpages 9
-#define Anum_pg_class_reltuples 10
-#define Anum_pg_class_relallvisible 11
-#define Anum_pg_class_reltoastrelid 12
-#define Anum_pg_class_relhasindex 13
-#define Anum_pg_class_relisshared 14
-#define Anum_pg_class_relpersistence 15
-#define Anum_pg_class_relkind 16
-#define Anum_pg_class_relnatts 17
-#define Anum_pg_class_relchecks 18
-#define Anum_pg_class_relhasoids 19
-#define Anum_pg_class_relhaspkey 20
-#define Anum_pg_class_relhasrules 21
-#define Anum_pg_class_relhastriggers 22
-#define Anum_pg_class_relhassubclass 23
-#define Anum_pg_class_relrowsecurity 24
-#define Anum_pg_class_relispopulated 25
-#define Anum_pg_class_relreplident 26
-#define Anum_pg_class_relfrozenxid 27
-#define Anum_pg_class_relminmxid 28
-#define Anum_pg_class_relacl 29
-#define Anum_pg_class_reloptions 30
+#define Natts_pg_class 31
+#define Anum_pg_class_relname 1
+#define Anum_pg_class_relnamespace 2
+#define Anum_pg_class_reltype 3
+#define Anum_pg_class_reloftype 4
+#define Anum_pg_class_relowner 5
+#define Anum_pg_class_relam 6
+#define Anum_pg_class_relfilenode 7
+#define Anum_pg_class_reltablespace 8
+#define Anum_pg_class_relpages 9
+#define Anum_pg_class_reltuples 10
+#define Anum_pg_class_relallvisible 11
+#define Anum_pg_class_reltoastrelid 12
+#define Anum_pg_class_relhasindex 13
+#define Anum_pg_class_relisshared 14
+#define Anum_pg_class_relpersistence 15
+#define Anum_pg_class_relkind 16
+#define Anum_pg_class_relnatts 17
+#define Anum_pg_class_relchecks 18
+#define Anum_pg_class_relhasoids 19
+#define Anum_pg_class_relhaspkey 20
+#define Anum_pg_class_relhasrules 21
+#define Anum_pg_class_relhastriggers 22
+#define Anum_pg_class_relhassubclass 23
+#define Anum_pg_class_relrowsecurity 24
+#define Anum_pg_class_relforcerowsecurity 25
+#define Anum_pg_class_relispopulated 26
+#define Anum_pg_class_relreplident 27
+#define Anum_pg_class_relfrozenxid 28
+#define Anum_pg_class_relminmxid 29
+#define Anum_pg_class_relacl 30
+#define Anum_pg_class_reloptions 31
/* ----------------
* initial contents of pg_class
@@ -140,13 +142,13 @@ typedef FormData_pg_class *Form_pg_class;
* Note: "3" in the relfrozenxid column stands for FirstNormalTransactionId;
* similarly, "1" in relminmxid stands for FirstMultiXactId
*/
-DATA(insert OID = 1247 ( pg_type PGNSP 71 0 PGUID 0 0 0 0 0 0 0 f f p r 30 0 t f f f f f t n 3 1 _null_ _null_ ));
+DATA(insert OID = 1247 ( pg_type PGNSP 71 0 PGUID 0 0 0 0 0 0 0 f f p r 30 0 t f f f f f f t n 3 1 _null_ _null_ ));
DESCR("");
-DATA(insert OID = 1249 ( pg_attribute PGNSP 75 0 PGUID 0 0 0 0 0 0 0 f f p r 21 0 f f f f f f t n 3 1 _null_ _null_ ));
+DATA(insert OID = 1249 ( pg_attribute PGNSP 75 0 PGUID 0 0 0 0 0 0 0 f f p r 21 0 f f f f f f f t n 3 1 _null_ _null_ ));
DESCR("");
-DATA(insert OID = 1255 ( pg_proc PGNSP 81 0 PGUID 0 0 0 0 0 0 0 f f p r 29 0 t f f f f f t n 3 1 _null_ _null_ ));
+DATA(insert OID = 1255 ( pg_proc PGNSP 81 0 PGUID 0 0 0 0 0 0 0 f f p r 29 0 t f f f f f f t n 3 1 _null_ _null_ ));
DESCR("");
-DATA(insert OID = 1259 ( pg_class PGNSP 83 0 PGUID 0 0 0 0 0 0 0 f f p r 30 0 t f f f f f t n 3 1 _null_ _null_ ));
+DATA(insert OID = 1259 ( pg_class PGNSP 83 0 PGUID 0 0 0 0 0 0 0 f f p r 31 0 t f f f f f f t n 3 1 _null_ _null_ ));
DESCR("");
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index ff695aa..77cf8d7 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -287,6 +287,7 @@ extern int trace_recovery(int trace_level);
/* flags to be OR'd to form sec_context */
#define SECURITY_LOCAL_USERID_CHANGE 0x0001
#define SECURITY_RESTRICTED_OPERATION 0x0002
+#define SECURITY_NOFORCE_RLS 0x0004
extern char *DatabasePath;
@@ -305,6 +306,7 @@ extern void GetUserIdAndSecContext(Oid *userid, int *sec_context);
extern void SetUserIdAndSecContext(Oid userid, int sec_context);
extern bool InLocalUserIdChange(void);
extern bool InSecurityRestrictedOperation(void);
+extern bool InNoForceRLSOperation(void);
extern void GetUserIdAndContext(Oid *userid, bool *sec_def_context);
extern void SetUserIdAndContext(Oid userid, bool sec_def_context);
extern void InitializeSessionUserId(const char *rolename, Oid useroid);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 770866a..fdf19fa 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -1514,6 +1514,8 @@ typedef enum AlterTableType
AT_ReplicaIdentity, /* REPLICA IDENTITY */
AT_EnableRowSecurity, /* ENABLE ROW SECURITY */
AT_DisableRowSecurity, /* DISABLE ROW SECURITY */
+ AT_ForceRowSecurity, /* FORCE ROW SECURITY */
+ AT_NoForceRowSecurity, /* NO FORCE ROW SECURITY */
AT_GenericOptions /* OPTIONS (...) */
} AlterTableType;
diff --git a/src/test/modules/test_ddl_deparse/test_ddl_deparse.c b/src/test/modules/test_ddl_deparse/test_ddl_deparse.c
index e2dc4b5..4fca737 100644
--- a/src/test/modules/test_ddl_deparse/test_ddl_deparse.c
+++ b/src/test/modules/test_ddl_deparse/test_ddl_deparse.c
@@ -275,6 +275,12 @@ get_altertable_subcmdtypes(PG_FUNCTION_ARGS)
case AT_DisableRowSecurity:
strtype = "DISABLE ROW SECURITY";
break;
+ case AT_ForceRowSecurity:
+ strtype = "FORCE ROW SECURITY";
+ break;
+ case AT_NoForceRowSecurity:
+ strtype = "NO FORCE ROW SECURITY";
+ break;
case AT_GenericOptions:
strtype = "SET OPTIONS";
break;
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 0363dfd..a050444 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -3009,6 +3009,155 @@ SET SESSION AUTHORIZATION rls_regress_user0;
DROP TABLE r1;
DROP TABLE r2;
--
+-- FORCE ROW LEVEL SECURITY applies RLS to owners but
+-- only when row_security = on
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+SET row_security = on;
+CREATE TABLE r1 (a int);
+INSERT INTO r1 VALUES (10), (20);
+CREATE POLICY p1 ON r1 USING (false);
+ALTER TABLE r1 ENABLE ROW LEVEL SECURITY;
+ALTER TABLE r1 FORCE ROW LEVEL SECURITY;
+-- No error, but no rows
+TABLE r1;
+ a
+---
+(0 rows)
+
+-- RLS error
+INSERT INTO r1 VALUES (1);
+ERROR: new row violates row level security policy for "r1"
+-- No error (unable to see any rows to update)
+UPDATE r1 SET a = 1;
+TABLE r1;
+ a
+---
+(0 rows)
+
+-- No error (unable to see any rows to delete)
+DELETE FROM r1;
+TABLE r1;
+ a
+---
+(0 rows)
+
+SET row_security = off;
+-- Shows all rows
+TABLE r1;
+ a
+----
+ 10
+ 20
+(2 rows)
+
+-- Update all rows
+UPDATE r1 SET a = 1;
+TABLE r1;
+ a
+---
+ 1
+ 1
+(2 rows)
+
+-- Delete all rows
+DELETE FROM r1;
+TABLE r1;
+ a
+---
+(0 rows)
+
+DROP TABLE r1;
+--
+-- FORCE ROW LEVEL SECURITY does not break RI
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+SET row_security = on;
+CREATE TABLE r1 (a int PRIMARY KEY);
+CREATE TABLE r2 (a int REFERENCES r1);
+INSERT INTO r1 VALUES (10), (20);
+INSERT INTO r2 VALUES (10), (20);
+-- Create policies on r2 which prevent the
+-- owner from seeing any rows, but RI should
+-- still see them.
+CREATE POLICY p1 ON r2 USING (false);
+ALTER TABLE r2 ENABLE ROW LEVEL SECURITY;
+ALTER TABLE r2 FORCE ROW LEVEL SECURITY;
+-- Errors due to rows in r2
+DELETE FROM r1;
+ERROR: update or delete on table "r1" violates foreign key constraint "r2_a_fkey" on table "r2"
+DETAIL: Key (a)=(10) is still referenced from table "r2".
+-- Reset r2 to no-RLS
+DROP POLICY p1 ON r2;
+ALTER TABLE r2 NO FORCE ROW LEVEL SECURITY;
+ALTER TABLE r2 DISABLE ROW LEVEL SECURITY;
+-- clean out r2 for INSERT test below
+DELETE FROM r2;
+-- Change r1 to not allow rows to be seen
+CREATE POLICY p1 ON r1 USING (false);
+ALTER TABLE r1 ENABLE ROW LEVEL SECURITY;
+ALTER TABLE r1 FORCE ROW LEVEL SECURITY;
+-- No rows seen
+TABLE r1;
+ a
+---
+(0 rows)
+
+-- No error, RI still sees that row exists in r1
+INSERT INTO r2 VALUES (10);
+DROP TABLE r2;
+DROP TABLE r1;
+-- Ensure cascaded DELETE works
+CREATE TABLE r1 (a int PRIMARY KEY);
+CREATE TABLE r2 (a int REFERENCES r1 ON DELETE CASCADE);
+INSERT INTO r1 VALUES (10), (20);
+INSERT INTO r2 VALUES (10), (20);
+-- Create policies on r2 which prevent the
+-- owner from seeing any rows, but RI should
+-- still see them.
+CREATE POLICY p1 ON r2 USING (false);
+ALTER TABLE r2 ENABLE ROW LEVEL SECURITY;
+ALTER TABLE r2 FORCE ROW LEVEL SECURITY;
+-- Deletes all records from both
+DELETE FROM r1;
+-- Remove FORCE from r2
+ALTER TABLE r2 NO FORCE ROW LEVEL SECURITY;
+-- As owner, we now bypass RLS
+-- verify no rows in r2 now
+TABLE r2;
+ a
+---
+(0 rows)
+
+DROP TABLE r2;
+DROP TABLE r1;
+-- Ensure cascaded UPDATE works
+CREATE TABLE r1 (a int PRIMARY KEY);
+CREATE TABLE r2 (a int REFERENCES r1 ON UPDATE CASCADE);
+INSERT INTO r1 VALUES (10), (20);
+INSERT INTO r2 VALUES (10), (20);
+-- Create policies on r2 which prevent the
+-- owner from seeing any rows, but RI should
+-- still see them.
+CREATE POLICY p1 ON r2 USING (false);
+ALTER TABLE r2 ENABLE ROW LEVEL SECURITY;
+ALTER TABLE r2 FORCE ROW LEVEL SECURITY;
+-- Updates records in both
+UPDATE r1 SET a = a+5;
+-- Remove FORCE from r2
+ALTER TABLE r2 NO FORCE ROW LEVEL SECURITY;
+-- As owner, we now bypass RLS
+-- verify records in r2 updated
+TABLE r2;
+ a
+----
+ 15
+ 25
+(2 rows)
+
+DROP TABLE r2;
+DROP TABLE r1;
+--
-- Clean up objects
--
RESET SESSION AUTHORIZATION;
@@ -3031,3 +3180,10 @@ CREATE POLICY p1 ON rls_tbl USING (c1 > 5);
CREATE POLICY p2 ON rls_tbl FOR SELECT USING (c1 <= 3);
CREATE POLICY p3 ON rls_tbl FOR UPDATE USING (c1 <= 3) WITH CHECK (c1 > 5);
CREATE POLICY p4 ON rls_tbl FOR DELETE USING (c1 <= 3);
+CREATE TABLE rls_tbl_force (c1 int);
+ALTER TABLE rls_tbl_force ENABLE ROW LEVEL SECURITY;
+ALTER TABLE rls_tbl_force FORCE ROW LEVEL SECURITY;
+CREATE POLICY p1 ON rls_tbl_force USING (c1 = 5) WITH CHECK (c1 < 5);
+CREATE POLICY p2 ON rls_tbl_force FOR SELECT USING (c1 = 8);
+CREATE POLICY p3 ON rls_tbl_force FOR UPDATE USING (c1 = 8) WITH CHECK (c1 >= 5);
+CREATE POLICY p4 ON rls_tbl_force FOR DELETE USING (c1 = 8);
diff --git a/src/test/regress/output/misc.source b/src/test/regress/output/misc.source
index 4414763..5f263f9 100644
--- a/src/test/regress/output/misc.source
+++ b/src/test/regress/output/misc.source
@@ -672,6 +672,7 @@ SELECT user_relns() AS user_relns
real_city
reltime_tbl
rls_tbl
+ rls_tbl_force
road
shighway
slow_emp4000
@@ -709,7 +710,7 @@ SELECT user_relns() AS user_relns
tvvmv
varchar_tbl
xacttest
-(131 rows)
+(132 rows)
SELECT name(equipment(hobby_construct(text 'skywalking', text 'mer')));
name
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
index 7f8772f..070c452 100644
--- a/src/test/regress/sql/rowsecurity.sql
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -1289,6 +1289,141 @@ DROP TABLE r1;
DROP TABLE r2;
--
+-- FORCE ROW LEVEL SECURITY applies RLS to owners but
+-- only when row_security = on
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+SET row_security = on;
+CREATE TABLE r1 (a int);
+INSERT INTO r1 VALUES (10), (20);
+
+CREATE POLICY p1 ON r1 USING (false);
+ALTER TABLE r1 ENABLE ROW LEVEL SECURITY;
+ALTER TABLE r1 FORCE ROW LEVEL SECURITY;
+
+-- No error, but no rows
+TABLE r1;
+
+-- RLS error
+INSERT INTO r1 VALUES (1);
+
+-- No error (unable to see any rows to update)
+UPDATE r1 SET a = 1;
+TABLE r1;
+
+-- No error (unable to see any rows to delete)
+DELETE FROM r1;
+TABLE r1;
+
+SET row_security = off;
+-- Shows all rows
+TABLE r1;
+
+-- Update all rows
+UPDATE r1 SET a = 1;
+TABLE r1;
+
+-- Delete all rows
+DELETE FROM r1;
+TABLE r1;
+
+DROP TABLE r1;
+
+--
+-- FORCE ROW LEVEL SECURITY does not break RI
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+SET row_security = on;
+CREATE TABLE r1 (a int PRIMARY KEY);
+CREATE TABLE r2 (a int REFERENCES r1);
+INSERT INTO r1 VALUES (10), (20);
+INSERT INTO r2 VALUES (10), (20);
+
+-- Create policies on r2 which prevent the
+-- owner from seeing any rows, but RI should
+-- still see them.
+CREATE POLICY p1 ON r2 USING (false);
+ALTER TABLE r2 ENABLE ROW LEVEL SECURITY;
+ALTER TABLE r2 FORCE ROW LEVEL SECURITY;
+
+-- Errors due to rows in r2
+DELETE FROM r1;
+
+-- Reset r2 to no-RLS
+DROP POLICY p1 ON r2;
+ALTER TABLE r2 NO FORCE ROW LEVEL SECURITY;
+ALTER TABLE r2 DISABLE ROW LEVEL SECURITY;
+
+-- clean out r2 for INSERT test below
+DELETE FROM r2;
+
+-- Change r1 to not allow rows to be seen
+CREATE POLICY p1 ON r1 USING (false);
+ALTER TABLE r1 ENABLE ROW LEVEL SECURITY;
+ALTER TABLE r1 FORCE ROW LEVEL SECURITY;
+
+-- No rows seen
+TABLE r1;
+
+-- No error, RI still sees that row exists in r1
+INSERT INTO r2 VALUES (10);
+
+DROP TABLE r2;
+DROP TABLE r1;
+
+-- Ensure cascaded DELETE works
+CREATE TABLE r1 (a int PRIMARY KEY);
+CREATE TABLE r2 (a int REFERENCES r1 ON DELETE CASCADE);
+INSERT INTO r1 VALUES (10), (20);
+INSERT INTO r2 VALUES (10), (20);
+
+-- Create policies on r2 which prevent the
+-- owner from seeing any rows, but RI should
+-- still see them.
+CREATE POLICY p1 ON r2 USING (false);
+ALTER TABLE r2 ENABLE ROW LEVEL SECURITY;
+ALTER TABLE r2 FORCE ROW LEVEL SECURITY;
+
+-- Deletes all records from both
+DELETE FROM r1;
+
+-- Remove FORCE from r2
+ALTER TABLE r2 NO FORCE ROW LEVEL SECURITY;
+
+-- As owner, we now bypass RLS
+-- verify no rows in r2 now
+TABLE r2;
+
+DROP TABLE r2;
+DROP TABLE r1;
+
+-- Ensure cascaded UPDATE works
+CREATE TABLE r1 (a int PRIMARY KEY);
+CREATE TABLE r2 (a int REFERENCES r1 ON UPDATE CASCADE);
+INSERT INTO r1 VALUES (10), (20);
+INSERT INTO r2 VALUES (10), (20);
+
+-- Create policies on r2 which prevent the
+-- owner from seeing any rows, but RI should
+-- still see them.
+CREATE POLICY p1 ON r2 USING (false);
+ALTER TABLE r2 ENABLE ROW LEVEL SECURITY;
+ALTER TABLE r2 FORCE ROW LEVEL SECURITY;
+
+-- Updates records in both
+UPDATE r1 SET a = a+5;
+
+-- Remove FORCE from r2
+ALTER TABLE r2 NO FORCE ROW LEVEL SECURITY;
+
+-- As owner, we now bypass RLS
+-- verify records in r2 updated
+TABLE r2;
+
+DROP TABLE r2;
+DROP TABLE r1;
+
+--
-- Clean up objects
--
RESET SESSION AUTHORIZATION;
@@ -1315,3 +1450,11 @@ CREATE POLICY p1 ON rls_tbl USING (c1 > 5);
CREATE POLICY p2 ON rls_tbl FOR SELECT USING (c1 <= 3);
CREATE POLICY p3 ON rls_tbl FOR UPDATE USING (c1 <= 3) WITH CHECK (c1 > 5);
CREATE POLICY p4 ON rls_tbl FOR DELETE USING (c1 <= 3);
+
+CREATE TABLE rls_tbl_force (c1 int);
+ALTER TABLE rls_tbl_force ENABLE ROW LEVEL SECURITY;
+ALTER TABLE rls_tbl_force FORCE ROW LEVEL SECURITY;
+CREATE POLICY p1 ON rls_tbl_force USING (c1 = 5) WITH CHECK (c1 < 5);
+CREATE POLICY p2 ON rls_tbl_force FOR SELECT USING (c1 = 8);
+CREATE POLICY p3 ON rls_tbl_force FOR UPDATE USING (c1 = 8) WITH CHECK (c1 >= 5);
+CREATE POLICY p4 ON rls_tbl_force FOR DELETE USING (c1 = 8);
--
1.9.1
On Tue, Jul 28, 2015 at 04:04:29PM -0700, Joe Conway wrote:
On 07/27/2015 05:34 PM, Joe Conway wrote:
On 07/27/2015 01:13 PM, Alvaro Herrera wrote:
Hmm, these are not ACL objects, so conceptually it seems cleaner
to use a different symbol for this. I think the catalog state
and the error messages would be a bit confusing otherwise.Ok -- done
Pushed to HEAD and 9.5
I reviewed this commit, f781a0f "Create a pg_shdepend entry for each role in
TO clause of policies." This commit rendered the
http://www.postgresql.org/docs/devel/static/role-removal.html procedure[1]That page did not exist until 2015-10-07 (commit 1ea0c73), after the commit I'm reviewing here.
incomplete. Before dropping a role, one must additionally drop each policy
mentioning the role in pg_policy.polroles:
begin;
create role alice;
create table t (c int);
grant select on table t to alice;
create policy p0 on t to alice using (true);
reassign owned by alice to current_user;
drop owned by alice;
drop role alice;
rollback;
shdepDropOwned() ignores SHARED_DEPENDENCY_POLICY entries. Should it instead
remove the role from polroles, dropping the policy if that would empty
polroles? (Which should change, the documented role-removal procedure or the
DROP OWNED treatment of policies?) Independently,
http://www.postgresql.org/docs/devel/static/sql-drop-owned.html deserves an
update since it discusses every other object type having role dependencies.
Thanks,
nm
[1]: That page did not exist until 2015-10-07 (commit 1ea0c73), after the commit I'm reviewing here.
commit I'm reviewing here.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Noah,
* Noah Misch (noah@leadboat.com) wrote:
On Tue, Jul 28, 2015 at 04:04:29PM -0700, Joe Conway wrote:
Pushed to HEAD and 9.5
I reviewed this commit, f781a0f "Create a pg_shdepend entry for each role in
TO clause of policies."
Thanks for the review!
This commit rendered the
http://www.postgresql.org/docs/devel/static/role-removal.html procedure[1]
incomplete. Before dropping a role, one must additionally drop each policy
mentioning the role in pg_policy.polroles:begin;
create role alice;
create table t (c int);
grant select on table t to alice;
create policy p0 on t to alice using (true);
reassign owned by alice to current_user;
drop owned by alice;
drop role alice;
rollback;shdepDropOwned() ignores SHARED_DEPENDENCY_POLICY entries. Should it instead
remove the role from polroles, dropping the policy if that would empty
polroles? (Which should change, the documented role-removal procedure or the
DROP OWNED treatment of policies?)
I would expect the DROP OWNED treatment of policies to be similar to the
DROP OWNED treatment of GRANTs. I'm certainly of the opinion that this
is a bug which should be addressed. As an FYI, Joe's laptop recently
got stolen and he's working to get back up to speed as quickly as he
can. I've just put his new key into place on gitmaster (along with a
few other pginfra-related bits), but there's obviously a lot more for
him to be completely up and working again.
Independently,
http://www.postgresql.org/docs/devel/static/sql-drop-owned.html deserves an
update since it discusses every other object type having role dependencies.
Agreed.
Thanks!
Stephen