Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

allows to run 'barman switch-wal --force' when the user has the 'pg_checkpoint' role #845

Merged
merged 2 commits into from
Sep 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions barman/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,12 @@ class BackupFunctionsAccessRequired(PostgresException):
"""


class PostgresCheckpointPrivilegesRequired(PostgresException):
"""
Superuser or role 'pg_checkpoint' is required
"""


class PostgresIsInRecovery(PostgresException):
"""
PostgreSQL is in recovery, so no write operations are allowed
Expand Down
31 changes: 28 additions & 3 deletions barman/postgres.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
PostgresReplicationSlotInUse,
PostgresReplicationSlotsFull,
BackupFunctionsAccessRequired,
PostgresSuperuserRequired,
PostgresCheckpointPrivilegesRequired,
PostgresUnsupportedFeature,
)
from barman.infofile import Tablespace
Expand Down Expand Up @@ -638,6 +638,31 @@ def has_backup_privileges(self):
)
return None

@property
def has_checkpoint_privileges(self):
"""
Returns true if the current user is a superuser or if,
for PostgreSQL 14 and above, the user has the "pg_checkpoint" role.
"""

if self.server_version < 140000:
return self.is_superuser

if self.is_superuser:
return True
else:
role_check_query = "select pg_has_role(CURRENT_USER ,'pg_checkpoint', 'MEMBER');"
try:
cur = self._cursor()
cur.execute(role_check_query)
return cur.fetchone()[0]
except (PostgresConnectionError, psycopg2.Error) as e:
_logger.warning(
"Error checking privileges for functions needed for creating checkpoints: %s",
force_str(e).strip(),
)
return None

@property
def current_xlog_info(self):
"""
Expand Down Expand Up @@ -1351,8 +1376,8 @@ def checkpoint(self):
conn = self.connect()

# Requires superuser privilege
if not self.is_superuser:
raise PostgresSuperuserRequired()
if not self.has_checkpoint_privileges:
raise PostgresCheckpointPrivilegesRequired()

cur = conn.cursor()
cur.execute("CHECKPOINT")
Expand Down
6 changes: 4 additions & 2 deletions barman/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
PostgresReplicationSlotInUse,
PostgresReplicationSlotsFull,
PostgresSuperuserRequired,
PostgresCheckpointPrivilegesRequired,
PostgresUnsupportedFeature,
SyncError,
SyncNothingToDo,
Expand Down Expand Up @@ -2910,9 +2911,10 @@ def switch_wal(self, force=False, archive=None, archive_timeout=None):
"No switch performed because server '%s' "
"is a standby." % self.config.name
)
except PostgresSuperuserRequired:
except PostgresCheckpointPrivilegesRequired:
# Superuser rights are required to perform the switch_wal
output.error("Barman switch-wal requires superuser rights")
output.error("Barman switch-wal --force requires superuser rights or "
"the 'pg_checkpoint' role")
return

# If the user has asked to wait for a WAL file to be archived,
Expand Down
10 changes: 8 additions & 2 deletions doc/manual/21-preliminary_steps.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,14 @@ GRANT EXECUTE ON FUNCTION pg_backup_start(text, boolean) to barman;
GRANT EXECUTE ON FUNCTION pg_backup_stop(boolean) to barman;
```

It is worth noting that without a real superuser, the `--force` option
of the `barman switch-wal` command will not work.
It is worth noting that with PostgreSQL version 13 and below without a real
superuser, the `--force` option of the `barman switch-wal` command will not work.
If you are running PostgreSQL version 14 or above, you can grant the `pg_checkpoint`
role, so you can use this feature without a superuser:

``` sql
GRANT pg_checkpoint TO barman;
```

> **IMPORTANT:** The above `createuser` command will prompt for a password,
> which you are then advised to add to the `~barman/.pgpass` file
Expand Down
64 changes: 58 additions & 6 deletions tests/test_postgres.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
PostgresIsInRecovery,
BackupFunctionsAccessRequired,
PostgresObsoleteFeature,
PostgresSuperuserRequired,
PostgresCheckpointPrivilegesRequired,
PostgresUnsupportedFeature,
)
from barman.postgres import (
Expand Down Expand Up @@ -1057,22 +1057,74 @@ def test_get_remote_status(
"postgres_systemid": None,
}

@patch("barman.postgres.PostgreSQLConnection.connect")
@patch(
"barman.postgres.PostgreSQLConnection.is_superuser", new_callable=PropertyMock
)
@patch(
"barman.postgres.PostgreSQLConnection.server_version", new_callable=PropertyMock
)
def test_has_checkpoint_privileges(
self,
server_version_mock,
is_su_mock,
conn_mock
):
server = build_real_server()
cursor_mock = conn_mock.return_value.cursor.return_value

# test PostgreSQL 13 and below
server_version_mock.return_value = 139999
is_su_mock.return_value = False
assert not server.postgres.has_checkpoint_privileges
is_su_mock.return_value = True
assert server.postgres.has_checkpoint_privileges

# test PostgreSQL 14 and above
server_version_mock.return_value = 140000

# no superuser, no pg_checkpoint -> False
is_su_mock.return_value = False
cursor_mock.fetchone.side_effect = [(False,)]
assert not server.postgres.has_checkpoint_privileges
cursor_mock.execute.assert_called_with("select pg_has_role(CURRENT_USER ,'pg_checkpoint', 'MEMBER');")

# no superuser, pg_checkpoint -> True
cursor_mock.reset_mock()
is_su_mock.return_value = False
cursor_mock.fetchone.side_effect = [(True,)]
assert server.postgres.has_checkpoint_privileges
cursor_mock.execute.assert_called_with("select pg_has_role(CURRENT_USER ,'pg_checkpoint', 'MEMBER');")

# superuser, no pg_checkpoint -> True
cursor_mock.reset_mock()
is_su_mock.return_value = True
cursor_mock.fetchone.side_effect = [(False,)]
assert server.postgres.has_checkpoint_privileges

# superuser, pg_checkpoint -> True
cursor_mock.reset_mock()
is_su_mock.return_value = True
cursor_mock.fetchone.side_effect = [(True,)]
assert server.postgres.has_checkpoint_privileges

@patch("barman.postgres.PostgreSQLConnection.connect")
@patch(
"barman.postgres.PostgreSQLConnection.is_in_recovery", new_callable=PropertyMock
)
@patch(
"barman.postgres.PostgreSQLConnection.is_superuser", new_callable=PropertyMock
"barman.postgres.PostgreSQLConnection.has_checkpoint_privileges",
new_callable=PropertyMock
)
def test_checkpoint(self, is_superuser_mock, is_in_recovery_mock, conn_mock):
def test_checkpoint(self, has_cp_priv_mock, is_in_recovery_mock, conn_mock):
"""
Simple test for the execution of a checkpoint on a given server
"""
# Build a server
server = build_real_server()
cursor_mock = conn_mock.return_value.cursor.return_value
is_in_recovery_mock.return_value = False
is_superuser_mock.return_value = True
has_cp_priv_mock.return_value = True
# Execute the checkpoint method
server.postgres.checkpoint()
# Check for the right invocation
Expand All @@ -1081,8 +1133,8 @@ def test_checkpoint(self, is_superuser_mock, is_in_recovery_mock, conn_mock):
cursor_mock.reset_mock()
# Missing required permissions
is_in_recovery_mock.return_value = False
is_superuser_mock.return_value = False
with pytest.raises(PostgresSuperuserRequired):
has_cp_priv_mock.return_value = False
with pytest.raises(PostgresCheckpointPrivilegesRequired):
server.postgres.checkpoint()
assert not cursor_mock.execute.called

Expand Down