Skip to content

Commit ba419c7

Browse files
committed
Evict user info cache entries when roles change
1 parent 00043fa commit ba419c7

3 files changed

Lines changed: 36 additions & 11 deletions

File tree

api/core/security.py

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717
# Set up logger for this module
1818
logger = get_logger(__name__)
1919

20-
# TTL cache for token validation (1 hour TTL, max 1000 entries)
21-
_token_cache: cachetools.TTLCache[str, "UserInfo"] = cachetools.TTLCache(
20+
# TTL cache keyed by a user's OIDC subject. Evict entries when roles change. We
21+
# still validate the JWT signature and expiry on every request before reading a
22+
# cached record.
23+
_user_info_cache: cachetools.TTLCache[str, "UserInfo"] = cachetools.TTLCache(
2224
maxsize=1000, ttl=60 * 60
2325
)
2426

@@ -41,6 +43,17 @@ async def close_tdei_client() -> None:
4143
_tdei_client = None
4244

4345

46+
def evict_user_from_cache(auth_uid: str) -> None:
47+
"""
48+
Evict a user's cached UserInfo object so that their next request re-fetches
49+
permissions.
50+
51+
Call this after modifying a user's roles in the OSM DB to ensure the change
52+
takes effect on their next request rather than after the cache TTL expires.
53+
"""
54+
_user_info_cache.pop(auth_uid, None)
55+
56+
4457
security = HTTPBearer()
4558

4659

@@ -151,9 +164,13 @@ async def validate_token(
151164
osm_db_session: AsyncSession = Depends(get_osm_db_session),
152165
task_db_session: AsyncSession = Depends(get_task_db_session),
153166
) -> UserInfo:
154-
"""Dependency to get current authenticated user from TDEI/KeyCloak token and APIs.
167+
"""
168+
Dependency that gets the current authenticated user from the TDEI/KeyCloak
169+
access token and fetches permissions from TDEI APIs.
155170
156-
Results are cached by token for 1 hour to avoid repeated validation calls.
171+
The JWT signature and expiry are validated on every request. The expensive
172+
TDEI API and DB lookups are cached for 1 hour and should be evicted when a
173+
user's role changes via evict_user_from_cache().
157174
"""
158175
token = credentials.credentials
159176

@@ -172,16 +189,16 @@ async def validate_token(
172189
if user_id is None:
173190
raise credentials_exception
174191

175-
# Check cache first
176-
if token in _token_cache:
192+
# Cache keyed by user ID so roles take effect immediately after eviction:
193+
if user_id in _user_info_cache:
177194
logger.info("Token validation cache hit")
178-
return _token_cache[token]
195+
return _user_info_cache[user_id]
179196

180-
# Cache miss - perform full validation
197+
# Cache miss: fetch TDEI roles and DB data:
181198
user_info = await _validate_token_uncached(
182199
token, user_id, payload, osm_db_session, task_db_session
183200
)
184-
_token_cache[token] = user_info
201+
_user_info_cache[user_id] = user_info
185202

186203
return user_info
187204

api/src/users/routes.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from sqlmodel.ext.asyncio.session import AsyncSession
55

66
from api.core.database import get_osm_session
7-
from api.core.security import UserInfo, validate_token
7+
from api.core.security import UserInfo, evict_user_from_cache, validate_token
88
from api.src.users.repository import UserRepository
99
from api.src.users.schemas import SetRoleRequest, WorkspaceUserRoleItem
1010

@@ -48,6 +48,7 @@ async def assign_member_role(
4848
)
4949

5050
await user_repo.assign_member_role(workspace_id, user_id, body.role)
51+
evict_user_from_cache(str(user_id))
5152

5253

5354
@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
@@ -64,3 +65,4 @@ async def remove_member_role(
6465
)
6566

6667
await user_repo.remove_member_role(workspace_id, user_id)
68+
evict_user_from_cache(str(user_id))

api/src/workspaces/routes.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from api.core.database import get_osm_session, get_task_session
77
from api.core.logging import get_logger
8-
from api.core.security import UserInfo, validate_token
8+
from api.core.security import UserInfo, evict_user_from_cache, validate_token
99
from api.src.users.repository import UserRepository
1010
from api.src.users.schemas import WorkspaceUserRoleType
1111
from api.src.workspaces.repository import OSMRepository, WorkspaceRepository
@@ -148,6 +148,12 @@ async def create_workspace(
148148
WorkspaceUserRoleType.LEAD,
149149
)
150150

151+
# Evict the creator's cache so their next request reflects the new
152+
# workspace and lead role rather than serving stale data for up to
153+
# an hour:
154+
#
155+
evict_user_from_cache(str(current_user.user_uuid))
156+
151157
return workspace
152158
except Exception as e:
153159
logger.error(f"Failed to create workspace: {str(e)}")

0 commit comments

Comments
 (0)