diff --git a/CHANGELOG.md b/CHANGELOG.md index d942630d..f02766e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ Don't forget to remove deprecated code on each major release! - Verified full Windows compatibility via new CI workflows. - Add Django Signals support for backup and restore operations. New signals include `pre_backup`, `post_backup`, `pre_restore`, `post_restore`, `pre_media_backup`, `post_media_backup`, `pre_media_restore`, and `post_media_restore`. - New `DjangoConnector` that provides database-agnostic backup and restore functionality using Django's built-in `dumpdata` and `loaddata` management commands. +- Added `ENABLE_ROW_SECURITY` setting for PostgreSQL connectors to support databases with row-level security policies. ### Changed @@ -47,6 +48,7 @@ Don't forget to remove deprecated code on each major release! - Fix SQLite `no such table` errors. - Fix SQLite `UNIQUE constraint` errors. - Fix SQLite `index`/`trigger`/`view` ` already exists` errors. +- Fixed `pg_dump` error when backing up PostgreSQL databases with row-level security policies enabled. - Fix PostgreSQL restore errors with identity columns by automatically enabling `--if-exists` when using `--clean` in `PgDumpBinaryConnector`. ### Security diff --git a/dbbackup/db/postgresql.py b/dbbackup/db/postgresql.py index e781a35e..993652a6 100644 --- a/dbbackup/db/postgresql.py +++ b/dbbackup/db/postgresql.py @@ -32,6 +32,7 @@ class PgDumpConnector(BaseCommandDBConnector): restore_cmd = "psql" single_transaction = True drop = True + enable_row_security = False schemas: Optional[List[str]] = [] def _create_dump(self): @@ -45,6 +46,9 @@ def _create_dump(self): if self.drop: cmd += " --clean" + if self.enable_row_security: + cmd += " --enable-row-security" + if self.schemas: # First schema is not prefixed with -n # when using join function so add it manually. @@ -113,6 +117,7 @@ class PgDumpBinaryConnector(PgDumpConnector): single_transaction = True drop = True if_exists = False + enable_row_security = False pg_options = None def _create_dump(self): @@ -124,6 +129,9 @@ def _create_dump(self): for table in self.exclude: cmd += f" --exclude-table-data={table}" + if self.enable_row_security: + cmd += " --enable-row-security" + if self.schemas: cmd += " -n " + " -n ".join(self.schemas) diff --git a/docs/src/databases.md b/docs/src/databases.md index 44decb49..2ed25476 100644 --- a/docs/src/databases.md +++ b/docs/src/databases.md @@ -114,6 +114,17 @@ All PostgreSQL connectors have the following settings: | SINGLE_TRANSACTION | Wrap restore in a single transaction so errors cause full rollback (`--single-transaction` for `psql` / `pg_restore`). | `True` | | DROP | Include / execute drop statements when restoring (`--clean` with `pg_dump` / `pg_restore`). In binary mode drops happen during restore. | `True` | | IF_EXISTS | Add `IF EXISTS` to destructive statements in clean mode. Automatically enabled when `DROP=True` to prevent identity column errors. | `False` | +| ENABLE_ROW_SECURITY | Enable row-level security for dumping data (`--enable-row-security` with `pg_dump`). Required for databases with row-level security policies. | `False` | + +Example configuration for databases with row-level security: + +```python +DBBACKUP_CONNECTORS = { + 'default': { + 'ENABLE_ROW_SECURITY': True + } +} +``` #### PgDumpConnector diff --git a/tests/test_connectors/test_postgresql.py b/tests/test_connectors/test_postgresql.py index ed3f912a..d03fb930 100644 --- a/tests/test_connectors/test_postgresql.py +++ b/tests/test_connectors/test_postgresql.py @@ -172,6 +172,16 @@ def test_restore_dump_password_excluded(self, mock_dump_cmd): self.connector.restore_dump(dump) self.assertNotIn("secret", mock_dump_cmd.call_args[0][0]) + def test_create_dump_enable_row_security(self, mock_dump_cmd): + # Without enable_row_security + self.connector.enable_row_security = False + self.connector.create_dump() + self.assertNotIn(" --enable-row-security", mock_dump_cmd.call_args[0][0]) + # With enable_row_security + self.connector.enable_row_security = True + self.connector.create_dump() + self.assertIn(" --enable-row-security", mock_dump_cmd.call_args[0][0]) + @patch( "dbbackup.db.postgresql.PgDumpBinaryConnector.run_command", @@ -332,6 +342,16 @@ def test_restore_dump_password_excluded(self, mock_dump_cmd): self.connector.restore_dump(dump) self.assertNotIn("secret", mock_dump_cmd.call_args[0][0]) + def test_create_dump_enable_row_security(self, mock_dump_cmd): + # Without enable_row_security + self.connector.enable_row_security = False + self.connector.create_dump() + self.assertNotIn(" --enable-row-security", mock_dump_cmd.call_args[0][0]) + # With enable_row_security + self.connector.enable_row_security = True + self.connector.create_dump() + self.assertIn(" --enable-row-security", mock_dump_cmd.call_args[0][0]) + @patch( "dbbackup.db.postgresql.PgDumpGisConnector.run_command",