Skip to content

Commit

Permalink
Perform logical replication actions as the table owner.
Browse files Browse the repository at this point in the history
Up until now, logical replication actions have been performed as the
subscription owner, who will generally be a superuser.  Commit
cec57b1 documented hazards
associated with that situation, namely, that any user who owns a
table on the subscriber side could assume the privileges of the
subscription owner by attaching a trigger, expression index, or
some other kind of executable code to it. As a remedy, it suggested
not creating configurations where users who are not fully trusted
own tables on the subscriber.

Although that will work, it basically precludes using logical
replication in the way that people typically want to use it,
namely, to replicate a database from one node to another
without necessarily having any restrictions on which database
users can own tables. So, instead, change logical replication to
execute INSERT, UPDATE, DELETE, and TRUNCATE operations as the
table owner when they are replicated.

Since this involves switching the active user frequently within
a session that is authenticated as the subscription user, also
impose SECURITY_RESTRICTED_OPERATION restrictions on logical
replication code. As an exception, if the table owner can SET
ROLE to the subscription owner, these restrictions have no
security value, so don't impose them in that case.

Subscription owners are now required to have the ability to
SET ROLE to every role that owns a table that the subscription
is replicating. If they don't, replication will fail. Superusers,
who normally own subscriptions, satisfy this property by default.
Non-superusers users who own subscriptions will need to be
granted the roles that own relevant tables.

Patch by me, reviewed (but not necessarily in its entirety) by
Jelte Fennema, Jeff Davis, and Noah Misch.

Discussion: http://postgr.es/m/CA+TgmoaSCkg9ww9oppPqqs+9RVqCexYCE6Aq=UsYPfnOoDeFkw@mail.gmail.com
  • Loading branch information
robertmhaas committed Apr 4, 2023
1 parent 3077324 commit 1e10d49
Show file tree
Hide file tree
Showing 9 changed files with 250 additions and 109 deletions.
25 changes: 9 additions & 16 deletions doc/src/sgml/logical-replication.sgml
Original file line number Diff line number Diff line change
Expand Up @@ -1729,19 +1729,6 @@ CONTEXT: processing remote data for replication origin "pg_16395" during "INSER
<sect1 id="logical-replication-security">
<title>Security</title>

<para>
A user able to modify the schema of subscriber-side tables can execute
arbitrary code as the role which owns any subscription which modifies those tables. Limit ownership
and <literal>TRIGGER</literal> privilege on such tables to trusted roles.
Moreover, if untrusted users can create tables, use only
publications that list tables explicitly. That is to say, create a
subscription
<link linkend="sql-createpublication-for-all-tables"><literal>FOR ALL TABLES</literal></link>
or <link linkend="sql-createpublication-for-tables-in-schema"><literal>FOR TABLES IN SCHEMA</literal></link>
only when superusers trust every user permitted to create a non-temp table
on the publisher or the subscriber.
</para>

<para>
The role used for the replication connection must have
the <literal>REPLICATION</literal> attribute (or be a superuser). If the
Expand Down Expand Up @@ -1784,12 +1771,18 @@ CONTEXT: processing remote data for replication origin "pg_16395" during "INSER
</para>

<para>
To create a subscription, the user must be a superuser.
To create a subscription, the user must have the privileges of the
the <literal>pg_create_subscription</literal> role, as well as
<literal>CREATE</literal> privileges on the database.
</para>

<para>
The subscription apply process will run in the local database with the
privileges of the subscription owner.
The subscription apply process will, at a session level, run with the
privileges of the subscription owner. However, when performing an insert,
update, delete, or truncate operation on a particular table, it will switch
roles to the table owner and perform the operation with the table owner's
privileges. This means that the subscription owner needs to be able to
<literal>SET ROLE</literal> to each role that owns a replicated table.
</para>

<para>
Expand Down
20 changes: 18 additions & 2 deletions src/backend/commands/tablecmds.c
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@
#include "utils/syscache.h"
#include "utils/timestamp.h"
#include "utils/typcache.h"
#include "utils/usercontext.h"

/*
* ON COMMIT action list
Expand Down Expand Up @@ -1762,7 +1763,7 @@ ExecuteTruncate(TruncateStmt *stmt)
}

ExecuteTruncateGuts(rels, relids, relids_logged,
stmt->behavior, stmt->restart_seqs);
stmt->behavior, stmt->restart_seqs, false);

/* And close the rels */
foreach(cell, rels)
Expand Down Expand Up @@ -1790,7 +1791,8 @@ void
ExecuteTruncateGuts(List *explicit_rels,
List *relids,
List *relids_logged,
DropBehavior behavior, bool restart_seqs)
DropBehavior behavior, bool restart_seqs,
bool run_as_table_owner)
{
List *rels;
List *seq_relids = NIL;
Expand Down Expand Up @@ -1929,7 +1931,14 @@ ExecuteTruncateGuts(List *explicit_rels,
resultRelInfo = resultRelInfos;
foreach(cell, rels)
{
UserContext ucxt;

if (run_as_table_owner)
SwitchToUntrustedUser(resultRelInfo->ri_RelationDesc->rd_rel->relowner,
&ucxt);
ExecBSTruncateTriggers(estate, resultRelInfo);
if (run_as_table_owner)
RestoreUserContext(&ucxt);
resultRelInfo++;
}

Expand Down Expand Up @@ -2134,7 +2143,14 @@ ExecuteTruncateGuts(List *explicit_rels,
resultRelInfo = resultRelInfos;
foreach(cell, rels)
{
UserContext ucxt;

if (run_as_table_owner)
SwitchToUntrustedUser(resultRelInfo->ri_RelationDesc->rd_rel->relowner,
&ucxt);
ExecASTruncateTriggers(estate, resultRelInfo);
if (run_as_table_owner)
RestoreUserContext(&ucxt);
resultRelInfo++;
}

Expand Down
22 changes: 21 additions & 1 deletion src/backend/replication/logical/worker.c
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@
#include "utils/rls.h"
#include "utils/syscache.h"
#include "utils/timeout.h"
#include "utils/usercontext.h"

#define NAPTIME_PER_CYCLE 1000 /* max sleep time between cycles (1s) */

Expand Down Expand Up @@ -2395,6 +2396,7 @@ apply_handle_insert(StringInfo s)
LogicalRepRelMapEntry *rel;
LogicalRepTupleData newtup;
LogicalRepRelId relid;
UserContext ucxt;
ApplyExecutionData *edata;
EState *estate;
TupleTableSlot *remoteslot;
Expand Down Expand Up @@ -2423,6 +2425,9 @@ apply_handle_insert(StringInfo s)
return;
}

/* Make sure that any user-supplied code runs as the table owner. */
SwitchToUntrustedUser(rel->localrel->rd_rel->relowner, &ucxt);

/* Set relation for error callback */
apply_error_callback_arg.rel = rel;

Expand Down Expand Up @@ -2452,6 +2457,8 @@ apply_handle_insert(StringInfo s)
/* Reset relation for error callback */
apply_error_callback_arg.rel = NULL;

RestoreUserContext(&ucxt);

logicalrep_rel_close(rel, NoLock);

end_replication_step();
Expand Down Expand Up @@ -2530,6 +2537,7 @@ apply_handle_update(StringInfo s)
{
LogicalRepRelMapEntry *rel;
LogicalRepRelId relid;
UserContext ucxt;
ApplyExecutionData *edata;
EState *estate;
LogicalRepTupleData oldtup;
Expand Down Expand Up @@ -2569,6 +2577,9 @@ apply_handle_update(StringInfo s)
/* Check if we can do the update. */
check_relation_updatable(rel);

/* Make sure that any user-supplied code runs as the table owner. */
SwitchToUntrustedUser(rel->localrel->rd_rel->relowner, &ucxt);

/* Initialize the executor state. */
edata = create_edata_for_relation(rel);
estate = edata->estate;
Expand Down Expand Up @@ -2619,6 +2630,8 @@ apply_handle_update(StringInfo s)
/* Reset relation for error callback */
apply_error_callback_arg.rel = NULL;

RestoreUserContext(&ucxt);

logicalrep_rel_close(rel, NoLock);

end_replication_step();
Expand Down Expand Up @@ -2702,6 +2715,7 @@ apply_handle_delete(StringInfo s)
LogicalRepRelMapEntry *rel;
LogicalRepTupleData oldtup;
LogicalRepRelId relid;
UserContext ucxt;
ApplyExecutionData *edata;
EState *estate;
TupleTableSlot *remoteslot;
Expand Down Expand Up @@ -2736,6 +2750,9 @@ apply_handle_delete(StringInfo s)
/* Check if we can do the delete. */
check_relation_updatable(rel);

/* Make sure that any user-supplied code runs as the table owner. */
SwitchToUntrustedUser(rel->localrel->rd_rel->relowner, &ucxt);

/* Initialize the executor state. */
edata = create_edata_for_relation(rel);
estate = edata->estate;
Expand All @@ -2761,6 +2778,8 @@ apply_handle_delete(StringInfo s)
/* Reset relation for error callback */
apply_error_callback_arg.rel = NULL;

RestoreUserContext(&ucxt);

logicalrep_rel_close(rel, NoLock);

end_replication_step();
Expand Down Expand Up @@ -3211,7 +3230,8 @@ apply_handle_truncate(StringInfo s)
relids,
relids_logged,
DROP_RESTRICT,
restart_seqs);
restart_seqs,
true);
foreach(lc, remote_rels)
{
LogicalRepRelMapEntry *rel = lfirst(lc);
Expand Down
3 changes: 2 additions & 1 deletion src/backend/utils/init/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ include $(top_builddir)/src/Makefile.global
OBJS = \
globals.o \
miscinit.o \
postinit.o
postinit.o \
usercontext.o

include $(top_srcdir)/src/backend/common.mk
3 changes: 2 additions & 1 deletion src/backend/utils/init/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
backend_sources += files(
'globals.c',
'miscinit.c',
'postinit.c')
'postinit.c',
'usercontext.c')
92 changes: 92 additions & 0 deletions src/backend/utils/init/usercontext.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*-------------------------------------------------------------------------
*
* usercontext.c
* Convenience functions for running code as a different database user.
*
* Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
* Portions Copyright (c) 1994, Regents of the University of California
*
*
* IDENTIFICATION
* src/backend/utils/init/usercontext.c
*
*-------------------------------------------------------------------------
*/
#include "postgres.h"

#include "miscadmin.h"
#include "utils/acl.h"
#include "utils/guc.h"
#include "utils/usercontext.h"

/*
* Temporarily switch to a new user ID.
*
* If the current user doesn't have permission to SET ROLE to the new user,
* an ERROR occurs.
*
* If the new user doesn't have permission to SET ROLE to the current user,
* SECURITY_RESTRICTED_OPERATION is imposed and a new GUC nest level is
* created so that any settings changes can be rolled back.
*/
void
SwitchToUntrustedUser(Oid userid, UserContext *context)
{
/* Get the current user ID and security context. */
GetUserIdAndSecContext(&context->save_userid,
&context->save_sec_context);

/* Check that we have sufficient privileges to assume the target role. */
if (!member_can_set_role(context->save_userid, userid))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("role \"%s\" cannot SET ROLE to \"%s\"",
GetUserNameFromId(context->save_userid, false),
GetUserNameFromId(userid, false))));

/*
* Try to prevent the user to which we're switching from assuming the
* privileges of the current user, unless they can SET ROLE to that user
* anyway.
*/
if (member_can_set_role(userid, context->save_userid))
{
/*
* Each user can SET ROLE to the other, so there's no point in
* imposing any security restrictions. Just let the user do whatever
* they want.
*/
SetUserIdAndSecContext(userid, context->save_sec_context);
context->save_nestlevel = -1;
}
else
{
int sec_context = context->save_sec_context;

/*
* This user can SET ROLE to the target user, but not the other way
* around, so protect ourselves against the target user by setting
* SECURITY_RESTRICTED_OPERATION to prevent certain changes to the
* session state. Also set up a new GUC nest level, so that we can roll
* back any GUC changes that may be made by code running as the target
* user, inasmuch as they could be malicious.
*/
sec_context |= SECURITY_RESTRICTED_OPERATION;
SetUserIdAndSecContext(userid, sec_context);
context->save_nestlevel = NewGUCNestLevel();
}
}

/*
* Switch back to the original user ID.
*
* If we created a new GUC nest level, also role back any changes that were
* made within it.
*/
void
RestoreUserContext(UserContext *context)
{
if (context->save_nestlevel != -1)
AtEOXact_GUC(false, context->save_nestlevel);
SetUserIdAndSecContext(context->save_userid, context->save_sec_context);
}
3 changes: 2 additions & 1 deletion src/include/commands/tablecmds.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ extern void ExecuteTruncateGuts(List *explicit_rels,
List *relids,
List *relids_logged,
DropBehavior behavior,
bool restart_seqs);
bool restart_seqs,
bool run_as_table_owner);

extern void SetRelationHasSubclass(Oid relationId, bool relhassubclass);

Expand Down
26 changes: 26 additions & 0 deletions src/include/utils/usercontext.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*-------------------------------------------------------------------------
*
* usercontext.h
* Convenience functions for running code as a different database user.
*
*-------------------------------------------------------------------------
*/
#ifndef USERCONTEXT_H
#define USERCONTEXT_H

/*
* When temporarily changing to run as a different user, this structure
* holds the details needed to restore the original state.
*/
typedef struct UserContext
{
Oid save_userid;
int save_sec_context;
int save_nestlevel;
} UserContext;

/* Function prototypes. */
extern void SwitchToUntrustedUser(Oid userid, UserContext *context);
extern void RestoreUserContext(UserContext *context);

#endif /* USERCONTEXT_H */
Loading

0 comments on commit 1e10d49

Please sign in to comment.