feat(rbac): Redis L2 cache for permission checks

- load_roleperms: store role_perms to Redis (sc:rbac:role_perms, TTL=600s)
- get_userroles: store user roles to Redis (sc:rbac:user_roles:{userid}, TTL=300s)
- is_user_has_path_perm: check Redis L2 cache before opening DB connection
- Convert rp_caches values from list to set for O(1) path lookup
- Add cache_invalidate() to 7 event handlers for cross-worker invalidation via Pub/Sub
This commit is contained in:
yumoqing 2026-08-06 14:42:22 +08:00
parent 13ae4e89c8
commit 24faae3978

View File

@ -5,7 +5,7 @@ from ahserver.serverenv import ServerEnv
from appPublic.Singleton import SingletonDecorator from appPublic.Singleton import SingletonDecorator
from appPublic.log import debug, error from appPublic.log import debug, error
from appPublic.jsonConfig import getConfig from appPublic.jsonConfig import getConfig
from appPublic.share_cache import cache_get, cache_invalidate from appPublic.share_cache import cache_get, cache_set, cache_invalidate
def _cache_enabled(module_name='rbac'): def _cache_enabled(module_name='rbac'):
@ -114,6 +114,7 @@ class UserPermissions:
userid = getattr(data, 'id', None) userid = getattr(data, 'id', None)
if userid: if userid:
self.invalidate_user_cache(userid) self.invalidate_user_cache(userid)
asyncio.ensure_future(cache_invalidate('rbac', f'user_roles:{userid}'))
debug(f'RBAC cache invalidated for user id={userid} (users update)') debug(f'RBAC cache invalidated for user id={userid} (users update)')
except Exception as e: except Exception as e:
error(f'RBAC on_user_update handler error: {e}') error(f'RBAC on_user_update handler error: {e}')
@ -126,6 +127,7 @@ class UserPermissions:
userid = getattr(data, 'id', None) userid = getattr(data, 'id', None)
if userid: if userid:
self.invalidate_user_cache(userid) self.invalidate_user_cache(userid)
asyncio.ensure_future(cache_invalidate('rbac', f'user_roles:{userid}'))
debug(f'RBAC cache invalidated for user id={userid} (users create)') debug(f'RBAC cache invalidated for user id={userid} (users create)')
except Exception as e: except Exception as e:
error(f'RBAC on_user_create handler error: {e}') error(f'RBAC on_user_create handler error: {e}')
@ -138,6 +140,7 @@ class UserPermissions:
userid = getattr(data, 'id', None) userid = getattr(data, 'id', None)
if userid: if userid:
self.invalidate_user_cache(userid) self.invalidate_user_cache(userid)
asyncio.ensure_future(cache_invalidate('rbac', f'user_roles:{userid}'))
debug(f'RBAC cache invalidated for user id={userid} (users delete)') debug(f'RBAC cache invalidated for user id={userid} (users delete)')
except Exception as e: except Exception as e:
error(f'RBAC on_user_delete handler error: {e}') error(f'RBAC on_user_delete handler error: {e}')
@ -148,6 +151,7 @@ class UserPermissions:
""" """
try: try:
self.invalidate_rp_cache() self.invalidate_rp_cache()
asyncio.ensure_future(cache_invalidate('rbac', 'role_perms'))
debug('RBAC role-permission cache invalidated (rolepermission change)') debug('RBAC role-permission cache invalidated (rolepermission change)')
except Exception as e: except Exception as e:
error(f'RBAC on_rolepermission_change handler error: {e}') error(f'RBAC on_rolepermission_change handler error: {e}')
@ -158,6 +162,7 @@ class UserPermissions:
""" """
try: try:
self.invalidate_rp_cache() self.invalidate_rp_cache()
asyncio.ensure_future(cache_invalidate('rbac', 'role_perms'))
debug('RBAC role-permission cache invalidated (permission change)') debug('RBAC role-permission cache invalidated (permission change)')
except Exception as e: except Exception as e:
error(f'RBAC on_permission_change handler error: {e}') error(f'RBAC on_permission_change handler error: {e}')
@ -170,6 +175,7 @@ class UserPermissions:
try: try:
self.invalidate_all_user_caches() self.invalidate_all_user_caches()
self.invalidate_rp_cache() self.invalidate_rp_cache()
asyncio.ensure_future(cache_invalidate('rbac', 'role_perms'))
debug('RBAC all caches invalidated (role change)') debug('RBAC all caches invalidated (role change)')
except Exception as e: except Exception as e:
error(f'RBAC on_role_change handler error: {e}') error(f'RBAC on_role_change handler error: {e}')
@ -182,6 +188,7 @@ class UserPermissions:
userid = getattr(data, 'userid', None) userid = getattr(data, 'userid', None)
if userid: if userid:
self.invalidate_user_cache(userid) self.invalidate_user_cache(userid)
asyncio.ensure_future(cache_invalidate('rbac', f'user_roles:{userid}'))
debug(f'RBAC cache invalidated for user id={userid} (userrole change)') debug(f'RBAC cache invalidated for user id={userid} (userrole change)')
except Exception as e: except Exception as e:
error(f'RBAC on_userrole_change handler error: {e}') error(f'RBAC on_userrole_change handler error: {e}')
@ -271,8 +278,13 @@ order by c.orgtypeid, c.name"""
arr.append(r.path) arr.append(r.path)
new_caches[k] = arr new_caches[k] = arr
# Atomic swap: other coroutines see old cache or fully-loaded new cache, never {} # Atomic swap: other coroutines see old cache or fully-loaded new cache, never {}
self.rp_caches = new_caches self.rp_caches = {k: set(v) for k, v in new_caches.items()}
self.rp_cache_loaded_at = now self.rp_cache_loaded_at = now
# Share to Redis for cross-worker reuse
try:
await cache_set("rbac", "role_perms", new_caches, ttl=self.rp_cache_ttl)
except Exception:
pass
async def get_userroles(self, sor, userid): async def get_userroles(self, sor, userid):
"""Load user roles from database and cache them. """Load user roles from database and cache them.
@ -290,6 +302,11 @@ where a.id = c.userid
roles.append(f'*.{r.name}') roles.append(f'*.{r.name}')
roles = sorted(list(set(roles))) roles = sorted(list(set(roles)))
self.ur_caches.set(userid, roles) self.ur_caches.set(userid, roles)
# Share to Redis for cross-worker reuse
try:
await cache_set("rbac", f"user_roles:{userid}", roles, ttl=self.cache_ttl)
except Exception:
pass
return roles return roles
def check_roles_path(self, roles, path): def check_roles_path(self, roles, path):
@ -345,6 +362,19 @@ where a.id = c.userid
if userid is None: if userid is None:
roles = ['any', 'anonymous'] roles = ['any', 'anonymous']
# L2: Try shared Redis cache before opening DB connection
if _cache_enabled('rbac'):
if self.rp_caches is None:
cached_rp = await cache_get('rbac', 'role_perms', ttl=self.rp_cache_ttl)
if cached_rp is not None:
self.rp_caches = {k: set(v) for k, v in cached_rp.items()}
self.rp_cache_loaded_at = __import__('time').time()
if not roles and userid is not None:
cached_roles = await cache_get('rbac', f'user_roles:{userid}', ttl=self.cache_ttl)
if cached_roles is not None:
roles = cached_roles
self.ur_caches.set(userid, roles)
if not _cache_enabled('rbac') or self.rp_caches is None or not roles: if not _cache_enabled('rbac') or self.rp_caches is None or not roles:
env = ServerEnv() env = ServerEnv()
async with get_sor_context(env, 'rbac') as sor: async with get_sor_context(env, 'rbac') as sor: