sqlExe can return [] without raising an exception (bad connection,
cursor issue). When load_roleperms gets 0 records but had valid
cache before, keep the old cache instead of replacing with {}.
Prevents intermittent 403 from transient DB issues.
load_roleperms() was setting self.rp_caches = {} before the async
DB query. During the await, other coroutines saw {} (not None),
skipped the load, and checked permissions against an empty dict,
causing intermittent 403 on random paths.
Fix: build in local dict first, assign atomically when complete.
load_path.py scripts across modules register paths like '/module/api/%'
using SQL LIKE wildcard, but check_roles_path() only recognized '**' as
wildcard suffix. This caused all %-terminated paths to be treated as
exact matches, resulting in 403 for any sub-path.
Now both '/module/api/%' and '/module/api/**' work as prefix wildcards.
When module_cache.rbac=false in config.json, LRUCache.get() always returns
None and LRUCache.set() is a no-op. This caused get_userroles() to store
roles into a disabled cache, then callers read back None, leading to
TypeError in check_roles_path() when iterating over None.
Fix: get_userroles() now returns the roles list directly. Callers use the
return value instead of relying solely on cache reads. Added safety
fallback to deny access if roles is somehow still None.
- 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)
- Replace DATE_SUB(NOW(), INTERVAL 300 SECOND) with Python-level time check
- Replace NOW() with parameterized timestamps from Python
- Lockout check now done in _is_locked() function (DB-agnostic)
- All UPDATE statements use parameterized values, not DB functions
- Works with MySQL, PostgreSQL, SQLite, SQL Server, Oracle