From 1bb3d3f7f986b0c97126599701ebcb6973895901 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Dec 2025 14:12:34 +0000 Subject: [PATCH 1/7] Initial plan From 0bb2678372507d490d536ec256e61378b0aa2f16 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Dec 2025 14:21:17 +0000 Subject: [PATCH 2/7] Add clear_policy() method to Adapter for database cleanup Co-authored-by: mserico <140243407+mserico@users.noreply.github.com> --- casbin_async_sqlalchemy_adapter/adapter.py | 24 +++++++++++ tests/test_adapter.py | 32 +++++++++++++++ tests/test_adapter_softdelete.py | 47 ++++++++++++++++++++++ 3 files changed, 103 insertions(+) diff --git a/casbin_async_sqlalchemy_adapter/adapter.py b/casbin_async_sqlalchemy_adapter/adapter.py index 66a8286..86ae337 100644 --- a/casbin_async_sqlalchemy_adapter/adapter.py +++ b/casbin_async_sqlalchemy_adapter/adapter.py @@ -268,6 +268,30 @@ async def save_policy(self, model): return True + async def clear_policy(self): + """Clears all policy rules from the storage (database). + + This method removes all records from the casbin_rule table. + If soft delete is enabled, it marks all records as deleted. + + Returns: + bool: True if successful, False otherwise. + """ + async with self._session_scope() as session: + if self.softdelete_attribute is None: + # Hard delete all records + stmt = delete(self._db_class) + await session.execute(stmt) + else: + # Soft delete all active records + stmt = select(self._db_class) + stmt = self._softdelete_query(stmt) + result = await session.execute(stmt) + lines = result.scalars().all() + for line in lines: + setattr(line, self.softdelete_attribute.name, True) + return True + async def add_policy(self, sec, ptype, rule): """adds a policy rule to the storage.""" await self._save_policy_line(ptype, rule) diff --git a/tests/test_adapter.py b/tests/test_adapter.py index 72e2f09..64b8276 100644 --- a/tests/test_adapter.py +++ b/tests/test_adapter.py @@ -390,6 +390,38 @@ async def test_update_filtered_policies(self): await e.update_filtered_policies([["bob", "data2", "read"]], 0, "bob") self.assertTrue(e.enforce("bob", "data2", "read")) + async def test_clear_policy(self): + """Test that clear_policy() removes all records from the database.""" + from sqlalchemy import func + + e = await get_enforcer() + adapter = e.get_adapter() + engine = adapter._engine + + # Verify there are policies in the database + async_session = async_sessionmaker(engine, expire_on_commit=False, class_=AsyncSession) + async with async_session() as s: + cnt = await s.execute(select(func.count()).select_from(CasbinRule)) + initial_count = cnt.scalar_one() + self.assertGreater(initial_count, 0, "There should be policies in the database before clearing") + + # Clear all policies from the database + await adapter.clear_policy() + + # Verify all policies are removed from the database + async with async_session() as s: + cnt = await s.execute(select(func.count()).select_from(CasbinRule)) + final_count = cnt.scalar_one() + self.assertEqual(final_count, 0, "All policies should be removed from the database") + + # Verify enforcer still works after clearing (can load empty policy) + await e.load_policy() + self.assertFalse(e.enforce("alice", "data1", "read")) + + # Verify we can add policies after clearing + await e.add_policy("eve", "data3", "read") + self.assertTrue(e.enforce("eve", "data3", "read")) + class TestBulkInsert(IsolatedAsyncioTestCase): async def test_add_policies_bulk_internal_session(self): diff --git a/tests/test_adapter_softdelete.py b/tests/test_adapter_softdelete.py index 1f3a2b8..9dc2578 100644 --- a/tests/test_adapter_softdelete.py +++ b/tests/test_adapter_softdelete.py @@ -342,3 +342,50 @@ async def test_load_filtered_policy_ignores_soft_deleted(self): self.assertFalse(e2.enforce("bob", "data2", "write")) # Other data2 policies should be loaded self.assertTrue(e2.enforce("data2_admin", "data2", "read")) + + async def test_clear_policy_with_softdelete(self): + """Test that clear_policy() marks all records as deleted when softdelete is enabled.""" + e = await self.get_enforcer() + adapter = e.get_adapter() + engine = adapter._engine + + # Verify there are policies in the database + async_session = async_sessionmaker(engine, expire_on_commit=False, class_=AsyncSession) + async with async_session() as s: + # Count total records (including soft-deleted) + total_result = await s.execute(select(CasbinRuleSoftDelete)) + total_count = len(total_result.scalars().all()) + self.assertGreater(total_count, 0, "There should be policies in the database before clearing") + + # Count non-deleted records + active_result = await s.execute(select(CasbinRuleSoftDelete).where(CasbinRuleSoftDelete.is_deleted == False)) + active_count = len(active_result.scalars().all()) + self.assertGreater(active_count, 0, "There should be active policies before clearing") + + # Clear all policies (soft delete) + await adapter.clear_policy() + + # Verify all active policies are now marked as deleted + async with async_session() as s: + # Total count should remain the same (soft delete) + total_result = await s.execute(select(CasbinRuleSoftDelete)) + total_after = len(total_result.scalars().all()) + self.assertEqual(total_count, total_after, "Total records should remain the same with soft delete") + + # Active count should be 0 + active_result = await s.execute(select(CasbinRuleSoftDelete).where(CasbinRuleSoftDelete.is_deleted == False)) + active_after = len(active_result.scalars().all()) + self.assertEqual(active_after, 0, "All policies should be marked as deleted") + + # All should be marked as deleted + deleted_result = await s.execute(select(CasbinRuleSoftDelete).where(CasbinRuleSoftDelete.is_deleted == True)) + deleted_after = len(deleted_result.scalars().all()) + self.assertEqual(deleted_after, total_count, "All policies should be marked as deleted") + + # Verify enforcer still works after clearing (can load empty policy) + await e.load_policy() + self.assertFalse(e.enforce("alice", "data1", "read")) + + # Verify we can add policies after clearing + await e.add_policy("eve", "data3", "read") + self.assertTrue(e.enforce("eve", "data3", "read")) From 284f1d8456bb3fbeba1dd36d1f7b49c08691eddb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Dec 2025 14:23:12 +0000 Subject: [PATCH 3/7] Move func import to top of file for better organization Co-authored-by: mserico <140243407+mserico@users.noreply.github.com> --- tests/test_adapter.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_adapter.py b/tests/test_adapter.py index 64b8276..f381934 100644 --- a/tests/test_adapter.py +++ b/tests/test_adapter.py @@ -17,7 +17,7 @@ from unittest import IsolatedAsyncioTestCase import casbin -from sqlalchemy import Column, Integer, String, select +from sqlalchemy import Column, Integer, String, select, func from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker from casbin_async_sqlalchemy_adapter import Adapter @@ -392,8 +392,6 @@ async def test_update_filtered_policies(self): async def test_clear_policy(self): """Test that clear_policy() removes all records from the database.""" - from sqlalchemy import func - e = await get_enforcer() adapter = e.get_adapter() engine = adapter._engine From c03e46893172be1df2c509bc615a75a7905019ff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Dec 2025 14:25:10 +0000 Subject: [PATCH 4/7] Complete implementation of clear_policy() method Co-authored-by: mserico <140243407+mserico@users.noreply.github.com> --- example.db | Bin 0 -> 8192 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 example.db diff --git a/example.db b/example.db new file mode 100644 index 0000000000000000000000000000000000000000..ed2fee578240e6e2dfeaf6df043570743a958fcc GIT binary patch literal 8192 zcmeI#ziYxk6bJA-2^0fTx2_KyOh8K2X7`jsrD Date: Wed, 10 Dec 2025 14:26:05 +0000 Subject: [PATCH 5/7] Remove example.db and add *.db to .gitignore Co-authored-by: mserico <140243407+mserico@users.noreply.github.com> --- .gitignore | 4 ++++ example.db | Bin 8192 -> 0 bytes 2 files changed, 4 insertions(+) delete mode 100644 example.db diff --git a/.gitignore b/.gitignore index 8acb2f2..103d80c 100644 --- a/.gitignore +++ b/.gitignore @@ -130,3 +130,7 @@ dmypy.json .idea/ *.iml + +# SQLite database files +*.db +*.db-journal diff --git a/example.db b/example.db deleted file mode 100644 index ed2fee578240e6e2dfeaf6df043570743a958fcc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8192 zcmeI#ziYxk6bJA-2^0fTx2_KyOh8K2X7`jsrD Date: Wed, 10 Dec 2025 14:46:21 +0000 Subject: [PATCH 6/7] Fix Black linter error - remove trailing whitespace in docstring Co-authored-by: mserico <140243407+mserico@users.noreply.github.com> --- casbin_async_sqlalchemy_adapter/adapter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/casbin_async_sqlalchemy_adapter/adapter.py b/casbin_async_sqlalchemy_adapter/adapter.py index 86ae337..884103f 100644 --- a/casbin_async_sqlalchemy_adapter/adapter.py +++ b/casbin_async_sqlalchemy_adapter/adapter.py @@ -270,10 +270,10 @@ async def save_policy(self, model): async def clear_policy(self): """Clears all policy rules from the storage (database). - + This method removes all records from the casbin_rule table. If soft delete is enabled, it marks all records as deleted. - + Returns: bool: True if successful, False otherwise. """ From adf8405f944ad85cdcf0952e85d5fbaebe5c1e77 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Dec 2025 15:03:40 +0000 Subject: [PATCH 7/7] Add clear_policy() API documentation to README Co-authored-by: mserico <140243407+mserico@users.noreply.github.com> --- README.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/README.md b/README.md index 28dda68..43dbe69 100644 --- a/README.md +++ b/README.md @@ -167,6 +167,36 @@ async with async_session() as session: await session.commit() ``` +## Clearing All Policies + +The adapter provides a `clear_policy()` method to remove all policy records from the database directly: + +```python +import casbin_async_sqlalchemy_adapter +import casbin +from sqlalchemy.ext.asyncio import create_async_engine + +# Setup +engine = create_async_engine('sqlite+aiosqlite:///test.db') +adapter = casbin_async_sqlalchemy_adapter.Adapter(engine) +await adapter.create_table() + +e = casbin.AsyncEnforcer('path/to/model.conf', adapter) +await e.load_policy() + +# Add some policies +await e.add_policy("alice", "data1", "read") +await e.add_policy("bob", "data2", "write") + +# Clear all policies from the database +await adapter.clear_policy() + +# Reload to verify - the enforcer will have no policies +await e.load_policy() +``` + +When soft deletion is enabled, `clear_policy()` marks all records as deleted instead of physically removing them. + ## Soft Deletion Support The adapter supports soft deletion, which marks records as deleted instead of physically removing them from the database. This is useful for: