Skip to content

Commit 90f334d

Browse files
committed
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.
1 parent e78dc6b commit 90f334d

File tree

19 files changed

+537
-64
lines changed

19 files changed

+537
-64
lines changed

doc/src/sgml/catalogs.sgml

+10
Original file line numberDiff line numberDiff line change
@@ -1971,6 +1971,16 @@
19711971
</entry>
19721972
</row>
19731973

1974+
<row>
1975+
<entry><structfield>relforcerowsecurity</structfield></entry>
1976+
<entry><type>bool</type></entry>
1977+
<entry></entry>
1978+
<entry>
1979+
True if row level security (when enabled) will also apply to table owner; see
1980+
<link linkend="catalog-pg-policy"><structname>pg_policy</structname></link> catalog
1981+
</entry>
1982+
</row>
1983+
19741984
<row>
19751985
<entry><structfield>relispopulated</structfield></entry>
19761986
<entry><type>bool</type></entry>

doc/src/sgml/ref/alter_table.sgml

+17
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ ALTER TABLE ALL IN TABLESPACE <replaceable class="PARAMETER">name</replaceable>
6161
ENABLE ALWAYS RULE <replaceable class="PARAMETER">rewrite_rule_name</replaceable>
6262
DISABLE ROW LEVEL SECURITY
6363
ENABLE ROW LEVEL SECURITY
64+
FORCE ROW LEVEL SECURITY
65+
NO FORCE ROW LEVEL SECURITY
6466
CLUSTER ON <replaceable class="PARAMETER">index_name</replaceable>
6567
SET WITHOUT CLUSTER
6668
SET WITH OIDS
@@ -431,6 +433,21 @@ ALTER TABLE ALL IN TABLESPACE <replaceable class="PARAMETER">name</replaceable>
431433
</listitem>
432434
</varlistentry>
433435

436+
<varlistentry>
437+
<term><literal>NO FORCE</literal>/<literal>FORCE ROW LEVEL SECURITY</literal></term>
438+
<listitem>
439+
<para>
440+
These forms control the application of row security policies belonging
441+
to the table when the user is the table owner. If enabled, row level
442+
security policies will be applied when the user is the table owner. If
443+
disabled (the default) then row level security will not be applied when
444+
the user is the table owner.
445+
See also
446+
<xref linkend="SQL-CREATEPOLICY">.
447+
</para>
448+
</listitem>
449+
</varlistentry>
450+
434451
<varlistentry>
435452
<term><literal>CLUSTER ON</literal></term>
436453
<listitem>

src/backend/catalog/heap.c

+1
Original file line numberDiff line numberDiff line change
@@ -802,6 +802,7 @@ InsertPgClassTuple(Relation pg_class_desc,
802802
values[Anum_pg_class_relhasrules - 1] = BoolGetDatum(rd_rel->relhasrules);
803803
values[Anum_pg_class_relhastriggers - 1] = BoolGetDatum(rd_rel->relhastriggers);
804804
values[Anum_pg_class_relrowsecurity - 1] = BoolGetDatum(rd_rel->relrowsecurity);
805+
values[Anum_pg_class_relforcerowsecurity - 1] = BoolGetDatum(rd_rel->relforcerowsecurity);
805806
values[Anum_pg_class_relhassubclass - 1] = BoolGetDatum(rd_rel->relhassubclass);
806807
values[Anum_pg_class_relispopulated - 1] = BoolGetDatum(rd_rel->relispopulated);
807808
values[Anum_pg_class_relreplident - 1] = CharGetDatum(rd_rel->relreplident);

src/backend/commands/tablecmds.c

+40
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,7 @@ static void ATExecReplicaIdentity(Relation rel, ReplicaIdentityStmt *stmt, LOCKM
418418
static void ATExecGenericOptions(Relation rel, List *options);
419419
static void ATExecEnableRowSecurity(Relation rel);
420420
static void ATExecDisableRowSecurity(Relation rel);
421+
static void ATExecForceNoForceRowSecurity(Relation rel, bool force_rls);
421422

422423
static void copy_relation_data(SMgrRelation rel, SMgrRelation dst,
423424
ForkNumber forkNum, char relpersistence);
@@ -2929,6 +2930,8 @@ AlterTableGetLockLevel(List *cmds)
29292930
case AT_SetNotNull:
29302931
case AT_EnableRowSecurity:
29312932
case AT_DisableRowSecurity:
2933+
case AT_ForceRowSecurity:
2934+
case AT_NoForceRowSecurity:
29322935
cmd_lockmode = AccessExclusiveLock;
29332936
break;
29342937

@@ -3354,6 +3357,8 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
33543357
case AT_DropOf: /* NOT OF */
33553358
case AT_EnableRowSecurity:
33563359
case AT_DisableRowSecurity:
3360+
case AT_ForceRowSecurity:
3361+
case AT_NoForceRowSecurity:
33573362
ATSimplePermissions(rel, ATT_TABLE);
33583363
/* These commands never recurse */
33593364
/* No command-specific prep needed */
@@ -3670,6 +3675,12 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab, Relation rel,
36703675
case AT_DisableRowSecurity:
36713676
ATExecDisableRowSecurity(rel);
36723677
break;
3678+
case AT_ForceRowSecurity:
3679+
ATExecForceNoForceRowSecurity(rel, true);
3680+
break;
3681+
case AT_NoForceRowSecurity:
3682+
ATExecForceNoForceRowSecurity(rel, false);
3683+
break;
36733684
case AT_GenericOptions:
36743685
ATExecGenericOptions(rel, (List *) cmd->def);
36753686
break;
@@ -11048,6 +11059,35 @@ ATExecDisableRowSecurity(Relation rel)
1104811059
heap_freetuple(tuple);
1104911060
}
1105011061

11062+
/*
11063+
* ALTER TABLE FORCE/NO FORCE ROW LEVEL SECURITY
11064+
*/
11065+
static void
11066+
ATExecForceNoForceRowSecurity(Relation rel, bool force_rls)
11067+
{
11068+
Relation pg_class;
11069+
Oid relid;
11070+
HeapTuple tuple;
11071+
11072+
relid = RelationGetRelid(rel);
11073+
11074+
pg_class = heap_open(RelationRelationId, RowExclusiveLock);
11075+
11076+
tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
11077+
11078+
if (!HeapTupleIsValid(tuple))
11079+
elog(ERROR, "cache lookup failed for relation %u", relid);
11080+
11081+
((Form_pg_class) GETSTRUCT(tuple))->relforcerowsecurity = force_rls;
11082+
simple_heap_update(pg_class, &tuple->t_self, tuple);
11083+
11084+
/* keep catalog indexes current */
11085+
CatalogUpdateIndexes(pg_class, tuple);
11086+
11087+
heap_close(pg_class, RowExclusiveLock);
11088+
heap_freetuple(tuple);
11089+
}
11090+
1105111091
/*
1105211092
* ALTER FOREIGN TABLE <name> OPTIONS (...)
1105311093
*/

src/backend/parser/gram.y

+14
Original file line numberDiff line numberDiff line change
@@ -2332,6 +2332,20 @@ alter_table_cmd:
23322332
n->subtype = AT_DisableRowSecurity;
23332333
$$ = (Node *)n;
23342334
}
2335+
/* ALTER TABLE <name> FORCE ROW LEVEL SECURITY */
2336+
| FORCE ROW LEVEL SECURITY
2337+
{
2338+
AlterTableCmd *n = makeNode(AlterTableCmd);
2339+
n->subtype = AT_ForceRowSecurity;
2340+
$$ = (Node *)n;
2341+
}
2342+
/* ALTER TABLE <name> NO FORCE ROW LEVEL SECURITY */
2343+
| NO FORCE ROW LEVEL SECURITY
2344+
{
2345+
AlterTableCmd *n = makeNode(AlterTableCmd);
2346+
n->subtype = AT_NoForceRowSecurity;
2347+
$$ = (Node *)n;
2348+
}
23352349
| alter_generic_options
23362350
{
23372351
AlterTableCmd *n = makeNode(AlterTableCmd);

src/backend/utils/adt/ri_triggers.c

+4-2
Original file line numberDiff line numberDiff line change
@@ -3014,7 +3014,8 @@ ri_PlanCheck(const char *querystr, int nargs, Oid *argtypes,
30143014
/* Switch to proper UID to perform check as */
30153015
GetUserIdAndSecContext(&save_userid, &save_sec_context);
30163016
SetUserIdAndSecContext(RelationGetForm(query_rel)->relowner,
3017-
save_sec_context | SECURITY_LOCAL_USERID_CHANGE);
3017+
save_sec_context | SECURITY_LOCAL_USERID_CHANGE |
3018+
SECURITY_NOFORCE_RLS);
30183019

30193020
/* Create the plan */
30203021
qplan = SPI_prepare(querystr, nargs, argtypes);
@@ -3134,7 +3135,8 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
31343135
/* Switch to proper UID to perform check as */
31353136
GetUserIdAndSecContext(&save_userid, &save_sec_context);
31363137
SetUserIdAndSecContext(RelationGetForm(query_rel)->relowner,
3137-
save_sec_context | SECURITY_LOCAL_USERID_CHANGE);
3138+
save_sec_context | SECURITY_LOCAL_USERID_CHANGE |
3139+
SECURITY_NOFORCE_RLS);
31383140

31393141
/* Finally we can run the query. */
31403142
spi_result = SPI_execute_snapshot(qplan,

src/backend/utils/init/miscinit.c

+17-1
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ GetAuthenticatedUserId(void)
341341
* GetUserIdAndSecContext/SetUserIdAndSecContext - get/set the current user ID
342342
* and the SecurityRestrictionContext flags.
343343
*
344-
* Currently there are two valid bits in SecurityRestrictionContext:
344+
* Currently there are three valid bits in SecurityRestrictionContext:
345345
*
346346
* SECURITY_LOCAL_USERID_CHANGE indicates that we are inside an operation
347347
* that is temporarily changing CurrentUserId via these functions. This is
@@ -359,6 +359,13 @@ GetAuthenticatedUserId(void)
359359
* where the called functions are really supposed to be side-effect-free
360360
* anyway, such as VACUUM/ANALYZE/REINDEX.
361361
*
362+
* SECURITY_NOFORCE_RLS indicates that we are inside an operation which should
363+
* ignore the FORCE ROW LEVEL SECURITY per-table indication. This is used to
364+
* ensure that FORCE RLS does not mistakenly break referential integrity
365+
* checks. Note that this is intentionally only checked when running as the
366+
* owner of the table (which should always be the case for referential
367+
* integrity checks).
368+
*
362369
* Unlike GetUserId, GetUserIdAndSecContext does *not* Assert that the current
363370
* value of CurrentUserId is valid; nor does SetUserIdAndSecContext require
364371
* the new value to be valid. In fact, these routines had better not
@@ -401,6 +408,15 @@ InSecurityRestrictedOperation(void)
401408
return (SecurityRestrictionContext & SECURITY_RESTRICTED_OPERATION) != 0;
402409
}
403410

411+
/*
412+
* InNoForceRLSOperation - are we ignoring FORCE ROW LEVEL SECURITY ?
413+
*/
414+
bool
415+
InNoForceRLSOperation(void)
416+
{
417+
return (SecurityRestrictionContext & SECURITY_NOFORCE_RLS) != 0;
418+
}
419+
404420

405421
/*
406422
* These are obsolete versions of Get/SetUserIdAndSecContext that are

src/backend/utils/misc/rls.c

+39-5
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ check_enable_rls(Oid relid, Oid checkAsUser, bool noError)
5555
HeapTuple tuple;
5656
Form_pg_class classform;
5757
bool relrowsecurity;
58+
bool relforcerowsecurity;
5859
Oid user_id = checkAsUser ? checkAsUser : GetUserId();
5960

6061
/* Nothing to do for built-in relations */
@@ -68,6 +69,7 @@ check_enable_rls(Oid relid, Oid checkAsUser, bool noError)
6869
classform = (Form_pg_class) GETSTRUCT(tuple);
6970

7071
relrowsecurity = classform->relrowsecurity;
72+
relforcerowsecurity = classform->relforcerowsecurity;
7173

7274
ReleaseSysCache(tuple);
7375

@@ -76,14 +78,46 @@ check_enable_rls(Oid relid, Oid checkAsUser, bool noError)
7678
return RLS_NONE;
7779

7880
/*
79-
* Table owners and BYPASSRLS users bypass RLS. Note that a superuser
80-
* qualifies as both. Return RLS_NONE_ENV to indicate that this decision
81-
* depends on the environment (in this case, the user_id).
81+
* BYPASSRLS users always bypass RLS. Note that superusers are always
82+
* considered to have BYPASSRLS.
83+
*
84+
* Return RLS_NONE_ENV to indicate that this decision depends on the
85+
* environment (in this case, the user_id).
8286
*/
83-
if (pg_class_ownercheck(relid, user_id) ||
84-
has_bypassrls_privilege(user_id))
87+
if (has_bypassrls_privilege(user_id))
8588
return RLS_NONE_ENV;
8689

90+
/*
91+
* Table owners generally bypass RLS, except if row_security=true and the
92+
* table has been set (by an owner) to FORCE ROW SECURITY, and this is not
93+
* a referential integrity check.
94+
*
95+
* Return RLS_NONE_ENV to indicate that this decision depends on the
96+
* environment (in this case, the user_id).
97+
*/
98+
if (pg_class_ownercheck(relid, user_id))
99+
{
100+
/*
101+
* If row_security=true and FORCE ROW LEVEL SECURITY has been set on
102+
* the relation then we return RLS_ENABLED to indicate that RLS should
103+
* still be applied. If we are in a SECURITY_NOFORCE_RLS context or if
104+
* row_security=false then we return RLS_NONE_ENV.
105+
*
106+
* The SECURITY_NOFORCE_RLS indicates that we should not apply RLS even
107+
* if the table has FORCE RLS set- IF the current user is the owner.
108+
* This is specifically to ensure that referential integrity checks are
109+
* able to still run correctly.
110+
*
111+
* This is intentionally only done after we have checked that the user
112+
* is the table owner, which should always be the case for referential
113+
* integrity checks.
114+
*/
115+
if (row_security && relforcerowsecurity && !InNoForceRLSOperation())
116+
return RLS_ENABLED;
117+
else
118+
return RLS_NONE_ENV;
119+
}
120+
87121
/* row_security GUC says to bypass RLS, but user lacks permission */
88122
if (!row_security && !noError)
89123
ereport(ERROR,

0 commit comments

Comments
 (0)