From 4f103000b94524d6b75f4f7b20700081fad9feeb Mon Sep 17 00:00:00 2001 From: yumoqing Date: Mon, 18 May 2026 12:42:17 +0800 Subject: [PATCH] feat: implement real-time cache invalidation via DB event binding - Fixed syntax errors in userperm.py __init__ (removed broken 'this' reference and incomplete method definition) - Added 7 production-grade event handlers on UserPermissions: - on_user_create/update/delete: invalidate specific user cache - on_rolepermission_change: invalidate role-permission cache - on_permission_change: invalidate role-permission cache - on_role_change: invalidate ALL user + role-permission caches - on_userrole_change: invalidate specific user cache by userid - Added _bind_rbac_events() in init.py with 13 event bindings covering: users C/U/D, rolepermission C/U/D, permission U, role C/U/D, userrole C/U/D - All handlers have try/except error isolation to prevent one failure from breaking other handlers - Events auto-dispatched by sqlor after C/U/D operations (no service restart needed) - Cleaned up unused imports (DBPools, exception) --- rbac/init.py | 42 +++++++++++++++++++++++ rbac/userperm.py | 86 ++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 125 insertions(+), 3 deletions(-) diff --git a/rbac/init.py b/rbac/init.py index 6a95f3b..eb14b49 100644 --- a/rbac/init.py +++ b/rbac/init.py @@ -1,5 +1,6 @@ from ahserver.auth_api import AuthAPI from ahserver.serverenv import ServerEnv +from sqlor.dbpools import DBPools from .orgs import ( get_platform_providers ) @@ -19,6 +20,7 @@ from rbac.set_role_perms import ( set_role_perm, set_role_perms ) +from appPublic.log import debug async def get_owner_orgid(*args, **kw): return '0' @@ -26,6 +28,37 @@ async def get_owner_orgid(*args, **kw): async def sor_get_owner_orgid(sor, orgid): return '0' +def _bind_rbac_events(dbpools, dbname, up): + """Bind database events to RBAC cache invalidation handlers. + + Events are dispatched by sqlor after C/U/D operations. + Format: {dbname}:{tablename}:{c|u|d}:after + """ + bindings = [ + # users table: invalidate specific user cache on C/U/D + (f'{dbname}.users:c:after', up.on_user_create), + (f'{dbname}.users:u:after', up.on_user_update), + (f'{dbname}.users:d:after', up.on_user_delete), + # rolepermission table: invalidate role-permission cache on any change + (f'{dbname}.rolepermission:c:after', up.on_rolepermission_change), + (f'{dbname}.rolepermission:u:after', up.on_rolepermission_change), + (f'{dbname}.rolepermission:d:after', up.on_rolepermission_change), + # permission table: invalidate role-permission cache on update + (f'{dbname}.permission:u:after', up.on_permission_change), + # role table: invalidate ALL caches (affects all users) + (f'{dbname}.role:c:after', up.on_role_change), + (f'{dbname}.role:u:after', up.on_role_change), + (f'{dbname}.role:d:after', up.on_role_change), + # userrole table: invalidate specific user cache based on userid + (f'{dbname}.userrole:c:after', up.on_userrole_change), + (f'{dbname}.userrole:u:after', up.on_userrole_change), + (f'{dbname}.userrole:d:after', up.on_userrole_change), + ] + for event_name, handler in bindings: + dbpools.bind(event_name, handler) + debug(f'RBAC event bound: {event_name}') + + def load_rbac(): AuthAPI.checkUserPermission = objcheckperm env = ServerEnv() @@ -47,3 +80,12 @@ def load_rbac(): env.invalidate_user_perm_cache = env.userpermissions.invalidate_user_cache env.invalidate_all_perm_caches = env.userpermissions.invalidate_all_user_caches env.invalidate_role_perm_cache = env.userpermissions.invalidate_rp_cache + + # Bind database events for automatic cache invalidation + dbpools = DBPools() + dbname = env.get_module_dbname('rbac') + if dbname: + _bind_rbac_events(dbpools, dbname, env.userpermissions) + debug(f'RBAC event listeners bound for database: {dbname}') + else: + debug('RBAC event listeners skipped: no database configured for rbac module') diff --git a/rbac/userperm.py b/rbac/userperm.py index 09a8c39..5fc3c00 100644 --- a/rbac/userperm.py +++ b/rbac/userperm.py @@ -1,9 +1,9 @@ import asyncio from collections import OrderedDict -from sqlor.dbpools import DBPools, get_sor_context +from sqlor.dbpools import get_sor_context from ahserver.serverenv import ServerEnv from appPublic.Singleton import SingletonDecorator -from appPublic.log import debug, exception, error +from appPublic.log import debug, error class LRUCache: """Async-safe LRU cache with TTL support. @@ -80,7 +80,87 @@ class UserPermissions: # Async lock for rp_caches initialization (lazy init) self._rp_lock = None - + + def on_user_update(self, data): + """Event handler for users table update. + Clears the specific user's permission cache. + """ + try: + userid = getattr(data, 'id', None) + if userid: + self.invalidate_user_cache(userid) + debug(f'RBAC cache invalidated for user id={userid} (users update)') + except Exception as e: + error(f'RBAC on_user_update handler error: {e}') + + def on_user_create(self, data): + """Event handler for users table insert. + Clears the specific user's permission cache. + """ + try: + userid = getattr(data, 'id', None) + if userid: + self.invalidate_user_cache(userid) + debug(f'RBAC cache invalidated for user id={userid} (users create)') + except Exception as e: + error(f'RBAC on_user_create handler error: {e}') + + def on_user_delete(self, data): + """Event handler for users table delete. + Clears the specific user's permission cache. + """ + try: + userid = getattr(data, 'id', None) + if userid: + self.invalidate_user_cache(userid) + debug(f'RBAC cache invalidated for user id={userid} (users delete)') + except Exception as e: + error(f'RBAC on_user_delete handler error: {e}') + + def on_rolepermission_change(self, data): + """Event handler for rolepermission table C/U/D. + Clears the role-permission cache. + """ + try: + self.invalidate_rp_cache() + debug('RBAC role-permission cache invalidated (rolepermission change)') + except Exception as e: + error(f'RBAC on_rolepermission_change handler error: {e}') + + def on_permission_change(self, data): + """Event handler for permission table update. + Clears the role-permission cache. + """ + try: + self.invalidate_rp_cache() + debug('RBAC role-permission cache invalidated (permission change)') + except Exception as e: + error(f'RBAC on_permission_change handler error: {e}') + + def on_role_change(self, data): + """Event handler for role table C/U/D. + Clears all user caches and role-permission cache, + since role changes may affect any user. + """ + try: + self.invalidate_all_user_caches() + self.invalidate_rp_cache() + debug('RBAC all caches invalidated (role change)') + except Exception as e: + error(f'RBAC on_role_change handler error: {e}') + + def on_userrole_change(self, data): + """Event handler for userrole table C/U/D. + Clears the specific user's permission cache based on userid. + """ + try: + userid = getattr(data, 'userid', None) + if userid: + self.invalidate_user_cache(userid) + debug(f'RBAC cache invalidated for user id={userid} (userrole change)') + except Exception as e: + error(f'RBAC on_userrole_change handler error: {e}') + def _get_rp_lock(self): if self._rp_lock is None: self._rp_lock = asyncio.Lock()