How it works now
Right now, this module require to create adapter EVERY time the transaction occurs (ref). However, what we need is to be able to provide already generated session to the Adapter's methods. There is no point to re-create adapter itself, the DB URI likely won't ever change.
Problem
In terms of atomic DB-operations, Casbin adapter is a bit "difficult" to work with.
For example, when a User created, we want to assign some policies.
async def create_user(session, ...):
user = ... # Handled via session, but not yet commited...
await session.refresh(user)
# Since AsyncAdapter uses `self._session_scope()` all the time, we can't control the flow whatsoever...
await enforcer.add_group(...) # Assign `user.id` to a group...
await enforcer.add_policies(...) # Assign permissions to `user.id`...
# Something happens, rollback. No user is created in DB...
# But group is assigned to non-existing ID and this ID has permissions...
And here our atomicity breaks completely, because we haven't yet commited created user and a rollback might happen. If rollback occurs, we have to clean up manually.
Solution (?)
Provide 2 parameters parameters to most of the methods to handle transactions to DB:
session: AsyncSession | None = None ― either use provided session or use self._session_scope() to generate a new session;
commit: bool = True ― whether we need to commit or not.
Full list of the methods:
Adapter.add_policy(<previous signature>, commit: bool = True, session: AsyncSession | None = None)
Adapter.add_policies(<previous signature>, commit: bool = True, session: AsyncSession | None = None)
Adapter.remove_policy(<previous signature>, commit: bool = True, session: AsyncSession | None = None)
Adapter.remove_policies(<previous signature>, commit: bool = True, session: AsyncSession | None = None)
Adapter.remove_filtered_policy(<previous signature>, commit: bool = True, session: AsyncSession | None = None)
Adapter.update_policy(<previous signature>, commit: bool = True, session: AsyncSession | None = None)
Adapter.update_policies(<previous signature>, commit: bool = True, session: AsyncSession | None = None)
Adapter.update_filtered_policies(<previous signature>, commit: bool = True, session: AsyncSession | None = None)
And the session-related method should now become aware of externally used session provided for/within methods (not constructor).
@asynccontextmanager
async def _session_scope(self, commit: bool = True, session: AsyncSesssion | None = None) -> AsyncSession:
"""Provide an asynchronous transactional scope around a series of operations."""
# Instead of using a session passed in the constructor, let's pass it
if session:
yield session
else:
# Use internal session with automatic commit/rollback
async with self.session_local() as session:
try:
yield session
if commit:
await session.commit()
except Exception as e:
await session.rollback()
raise e
How it works now
Right now, this module require to create adapter EVERY time the transaction occurs (ref). However, what we need is to be able to provide already generated session to the
Adapter's methods. There is no point to re-create adapter itself, the DB URI likely won't ever change.Problem
In terms of atomic DB-operations, Casbin adapter is a bit "difficult" to work with.
For example, when a User created, we want to assign some policies.
And here our atomicity breaks completely, because we haven't yet commited created
userand a rollback might happen. If rollback occurs, we have to clean up manually.Solution (?)
Provide 2 parameters parameters to most of the methods to handle transactions to DB:
session: AsyncSession | None = None― either use providedsessionor useself._session_scope()to generate a new session;commit: bool = True― whether we need to commit or not.Full list of the methods:
Adapter.add_policy(<previous signature>, commit: bool = True, session: AsyncSession | None = None)Adapter.add_policies(<previous signature>, commit: bool = True, session: AsyncSession | None = None)Adapter.remove_policy(<previous signature>, commit: bool = True, session: AsyncSession | None = None)Adapter.remove_policies(<previous signature>, commit: bool = True, session: AsyncSession | None = None)Adapter.remove_filtered_policy(<previous signature>, commit: bool = True, session: AsyncSession | None = None)Adapter.update_policy(<previous signature>, commit: bool = True, session: AsyncSession | None = None)Adapter.update_policies(<previous signature>, commit: bool = True, session: AsyncSession | None = None)Adapter.update_filtered_policies(<previous signature>, commit: bool = True, session: AsyncSession | None = None)And the session-related method should now become aware of externally used session provided for/within methods (not constructor).