From 1c646f8d4380640d04efd701d569bec1fc7180aa Mon Sep 17 00:00:00 2001 From: WhoamiI00 Date: Tue, 25 Aug 2026 22:54:29 +0530 Subject: [PATCH 1/2] fix(api): carry the full project id in cache and lock keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `_pack` cut every scope segment down to the last 12 characters of the id, so two projects whose ids share that suffix shared every cache entry in every namespace that did not opt out — including `check_permissions` and `check_action_access`, which decide authorization. Project ids are server-generated UUID4s, so a caller cannot steer a collision and the odds of one arising are remote, but one project reading another's cached permission result is not a risk worth carrying by default. Ids are carried whole now. Readers, writers, the pattern branch of `invalidate_cache` and the lock keys all derive from this one function, so they move together and no namespace is left unable to clear its own entries. The dash padding stays, so an absent or short id produces the same fixed-width segment it always did. The lock namespace is the one place the key shape cannot simply change: during a rolling deploy, pods still on the previous release take the truncated key, and a lock held only under the new key would not exclude them. Lock operations therefore cover both keys for one release, claiming the legacy key first — a pod on the previous release sets only that one, so taking it is what makes the two generations exclude each other. That cover keeps colliding projects serializing against each other on locks until it is removed, which is deliberate: letting two pods into the same critical section is worse than two unrelated tenants queueing. Cache keys, where the permission caches live, separate immediately. Closes #6166 --- api/oss/src/utils/caching.py | 44 ++- api/oss/src/utils/locking.py | 127 ++++++-- .../unit/utils/test_cache_key_tenancy.py | 285 ++++++++++++++++++ 3 files changed, 420 insertions(+), 36 deletions(-) create mode 100644 api/oss/tests/pytest/unit/utils/test_cache_key_tenancy.py diff --git a/api/oss/src/utils/caching.py b/api/oss/src/utils/caching.py index b2c6f97b62..c8e5f3d994 100644 --- a/api/oss/src/utils/caching.py +++ b/api/oss/src/utils/caching.py @@ -40,24 +40,46 @@ # HELPERS ---------------------------------------------------------------------- +AGENTA_CACHE_SCOPE_WIDTH = 12 # minimum width of a scope segment + + +def _scope( + value: Optional[str], + legacy_truncated: Optional[bool] = False, +) -> str: + """One scope segment (`p:` or `u:`) of a cache key. + + Scope segments used to be cut down to their last 12 characters. Two projects whose + ids share that suffix then shared every cache entry in every namespace — including + `check_permissions` and `check_action_access`, which decide authorization. Ids are + server-generated UUID4s so a caller cannot steer a collision, but one project reading + another's cached permission result is not a thing to leave to chance. The id is + carried whole now. + + The padding stays: an absent or short id still produces the same fixed-width segment + it always did, so key shape is unchanged everywhere the id was not being cut. + + `legacy_truncated` reproduces the pre-change shape and exists only so lock holders + can span a rolling deploy — see `oss.src.utils.locking`. + """ + value = value or "" + + if legacy_truncated and len(value) > AGENTA_CACHE_SCOPE_WIDTH: + value = value[-AGENTA_CACHE_SCOPE_WIDTH:] + + return value + "-" * (AGENTA_CACHE_SCOPE_WIDTH - len(value)) + + def _pack( namespace: Optional[str] = None, key: Optional[Union[str, dict]] = None, project_id: Optional[str] = None, user_id: Optional[str] = None, pattern: Optional[bool] = False, + legacy_truncated_scope: Optional[bool] = False, ) -> str: - if project_id: - project_id = project_id[-12:] if len(project_id) > 12 else project_id - else: - project_id = "" - project_id = project_id + "-" * (12 - len(project_id)) - - if user_id: - user_id = user_id[-12:] if len(user_id) > 12 else user_id - else: - user_id = "" - user_id = user_id + "-" * (12 - len(user_id)) + project_id = _scope(project_id, legacy_truncated_scope) + user_id = _scope(user_id, legacy_truncated_scope) namespace = namespace or ("" if not pattern else "*") diff --git a/api/oss/src/utils/locking.py b/api/oss/src/utils/locking.py index cf0f5c1460..fddb67fdf7 100644 --- a/api/oss/src/utils/locking.py +++ b/api/oss/src/utils/locking.py @@ -38,6 +38,73 @@ """ +# TRANSITIONAL: SPANNING THE FULL-PROJECT-ID DEPLOY ----------------------------- +# +# Scope segments in cache and lock keys used to carry only the last 12 characters of +# an id (see `caching._scope`). During a rolling deploy, pods still on the previous +# release take the truncated key, so a lock held only under the full-id key would not +# exclude them and mutual exclusion would be lost for the length of the deploy. +# Every lock operation therefore covers both keys for one release. +# +# REMOVE once no pod predating the full-id change is running: drop the second element +# of `_lock_keys` and the `legacy_key` branches below. That is the whole surface. + + +def _lock_keys( + namespace: str, + key: Optional[Union[str, dict]] = None, + project_id: Optional[str] = None, + user_id: Optional[str] = None, +) -> tuple[str, Optional[str]]: + """This release's lock key, and the previous release's key when it differs.""" + lock_key = _pack( + namespace=f"lock:{namespace}", + key=key, + project_id=project_id, + user_id=user_id, + ) + legacy_key = _pack( + namespace=f"lock:{namespace}", + key=key, + project_id=project_id, + user_id=user_id, + legacy_truncated_scope=True, + ) + + # Ids no longer than the segment width were never truncated, so the two shapes + # coincide and there is no second key to cover. + return lock_key, (legacy_key if legacy_key != lock_key else None) + + +async def _renew_if_owner(lock_key: str, owner: Optional[str], ttl: int) -> bool: + if owner: + return bool( + await _lock_engine.eval( + _LOCK_RENEW_IF_OWNER_SCRIPT, + 1, + lock_key, + owner, + str(ttl), + ) + ) + + return bool(await _lock_engine.expire(lock_key, ttl)) + + +async def _release_if_owner(lock_key: str, owner: Optional[str]) -> bool: + if owner: + return bool( + await _lock_engine.eval( + _LOCK_RELEASE_IF_OWNER_SCRIPT, + 1, + lock_key, + owner, + ) + ) + + return bool(await _lock_engine.delete(lock_key)) + + # LOCK-STORE PRIMITIVES -------------------------------------------------------- # # Thin pass-throughs to the lock Redis client for callers that manage their own @@ -117,14 +184,26 @@ async def acquire_lock( ) """ try: - lock_key = _pack( - namespace=f"lock:{namespace}", + lock_key, legacy_key = _lock_keys( + namespace=namespace, key=key, project_id=project_id, user_id=user_id, ) lock_owner = uuid4().hex + # The legacy key is claimed first: a pod on the previous release sets only that + # one, so taking it is what makes the two generations exclude each other. Claiming + # it second would let both generations hold their own key and enter together. + if legacy_key is not None: + if not await _lock_engine.set(legacy_key, lock_owner, nx=True, ex=ttl): + if LOCK_DEBUG: + log.debug( + "[lock] BLOCKED", + key=legacy_key, + ) + return None + # Atomic SET NX: Returns True if lock acquired, False if already held acquired = await _lock_engine.set(lock_key, lock_owner, nx=True, ex=ttl) @@ -137,6 +216,11 @@ async def acquire_lock( ) return lock_owner else: + # This caller is not entering the critical section, so it must not leave the + # legacy key held until its TTL — that would block everyone for `ttl`. + if legacy_key is not None: + await _release_if_owner(legacy_key, lock_owner) + if LOCK_DEBUG: log.debug( "[lock] BLOCKED", @@ -180,23 +264,19 @@ async def renew_lock( True if lock was renewed, False if lock has already expired or on error """ try: - lock_key = _pack( - namespace=f"lock:{namespace}", + lock_key, legacy_key = _lock_keys( + namespace=namespace, key=key, project_id=project_id, user_id=user_id, ) - if owner: - renewed = await _lock_engine.eval( - _LOCK_RENEW_IF_OWNER_SCRIPT, - 1, - lock_key, - owner, - str(ttl), - ) - else: - renewed = await _lock_engine.expire(lock_key, ttl) + renewed = await _renew_if_owner(lock_key, owner, ttl) + + # Held for as long as the lock itself, or a pod on the previous release would + # take it the moment it lapsed while this holder was still inside the section. + if legacy_key is not None: + await _renew_if_owner(legacy_key, owner, ttl) if renewed: if LOCK_DEBUG: @@ -250,22 +330,19 @@ async def release_lock( await release_lock(namespace="account-creation", key=email) """ try: - lock_key = _pack( - namespace=f"lock:{namespace}", + lock_key, legacy_key = _lock_keys( + namespace=namespace, key=key, project_id=project_id, user_id=user_id, ) - if owner: - deleted = await _lock_engine.eval( - _LOCK_RELEASE_IF_OWNER_SCRIPT, - 1, - lock_key, - owner, - ) - else: - deleted = await _lock_engine.delete(lock_key) + deleted = await _release_if_owner(lock_key, owner) + + # Released even when the primary was already gone: the two were taken together, + # so leaving this one behind would block the section for the rest of its TTL. + if legacy_key is not None: + await _release_if_owner(legacy_key, owner) if deleted: if LOCK_DEBUG: diff --git a/api/oss/tests/pytest/unit/utils/test_cache_key_tenancy.py b/api/oss/tests/pytest/unit/utils/test_cache_key_tenancy.py new file mode 100644 index 0000000000..37e7f8a96f --- /dev/null +++ b/api/oss/tests/pytest/unit/utils/test_cache_key_tenancy.py @@ -0,0 +1,285 @@ +"""Cache and lock keys must not merge two tenants that share an id suffix. + +Scope segments used to carry only the last 12 characters of an id, so two projects +whose ids ended the same way shared every cache entry in every namespace — including +`check_permissions` and `check_action_access`, which decide authorization (#6166). + +The lock tests use a real in-memory fakeredis instance so they run without an external +Redis process; they skip when `fakeredis` is not installed. +""" + +from unittest.mock import patch +from uuid import uuid4 + +import pytest +import pytest_asyncio + +from oss.src.utils.caching import _pack, _scope, AGENTA_CACHE_SCOPE_WIDTH +from oss.src.utils import locking + + +# Two distinct UUID4s contrived to end in the same 12 characters: the collision the old +# truncation produced. Server-generated ids make this astronomically unlikely rather than +# impossible, and the consequence is a cross-tenant read. +COLLIDING_SUFFIX = "b2c3d4e5f6a7" +PROJECT_A = f"11111111-1111-4111-8111-1111{COLLIDING_SUFFIX}" +PROJECT_B = f"22222222-2222-4222-8222-2222{COLLIDING_SUFFIX}" + +USER_A = f"33333333-3333-4333-8333-3333{COLLIDING_SUFFIX}" +USER_B = f"44444444-4444-4444-8444-4444{COLLIDING_SUFFIX}" + + +def _key(namespace="check_action_access", project_id=None, user_id=None, **kwargs): + return _pack( + namespace=namespace, + key="read", + project_id=project_id, + user_id=user_id, + **kwargs, + ) + + +# KEY SHAPE -------------------------------------------------------------------- + + +def test_projects_sharing_an_id_suffix_do_not_share_a_cache_key(): + assert _key(project_id=PROJECT_A) != _key(project_id=PROJECT_B) + + +def test_users_sharing_an_id_suffix_do_not_share_a_cache_key(): + assert _key(project_id=PROJECT_A, user_id=USER_A) != _key( + project_id=PROJECT_A, user_id=USER_B + ) + + +def test_the_whole_id_is_in_the_key(): + assert PROJECT_A in _key(project_id=PROJECT_A) + assert USER_A in _key(project_id=PROJECT_A, user_id=USER_A) + + +def test_an_invalidation_pattern_is_scoped_to_one_project(): + """`invalidate_cache` without a key scans a pattern; it must not span both projects.""" + pattern = _pack(project_id=PROJECT_A, pattern=True) + + assert PROJECT_A in pattern + assert PROJECT_B not in pattern + + +def test_a_short_or_absent_id_keeps_its_historical_segment(): + """Only ids that were being cut change shape; everything else is untouched.""" + assert _scope(None) == "-" * AGENTA_CACHE_SCOPE_WIDTH + assert _scope("") == "-" * AGENTA_CACHE_SCOPE_WIDTH + assert _scope("abc") == "abc" + "-" * (AGENTA_CACHE_SCOPE_WIDTH - 3) + + exactly_wide = "a" * AGENTA_CACHE_SCOPE_WIDTH + assert _scope(exactly_wide) == exactly_wide + # At or below the width, truncating was already a no-op, so both shapes agree. + assert _scope(exactly_wide, legacy_truncated=True) == _scope(exactly_wide) + + +def test_the_legacy_shape_still_collides(): + """The opt-in legacy shape reproduces the bug — that is what makes it transitional.""" + assert _key(project_id=PROJECT_A, legacy_truncated_scope=True) == _key( + project_id=PROJECT_B, legacy_truncated_scope=True + ) + + +# LOCKS ACROSS A ROLLING DEPLOY ------------------------------------------------ + + +@pytest_asyncio.fixture +async def fake_redis(): + """Point the lock engine at an in-memory redis, as `test_evaluation_runtime_locks`. + + fakeredis executes Lua only with the optional `lupa` backend, which is not a + dependency here, so the two ownership scripts are supplied as an equivalent shim. + Everything else — `acquire_lock`, `renew_lock`, `release_lock` — runs as written. + """ + fakeredis = pytest.importorskip("fakeredis") + aioredis = pytest.importorskip("fakeredis.aioredis") + engine = pytest.importorskip("oss.src.dbs.redis.shared.engine") + + server = fakeredis.FakeServer() + client = aioredis.FakeRedis(server=server, decode_responses=False) + + async def _eval(script, numkeys, *args): + """`GET`, compare against the owner token, then `EXPIRE` or `DEL`.""" + lock_key, owner, *rest = args + + if await client.get(lock_key) != owner.encode(): + return 0 + + if script == locking._LOCK_RENEW_IF_OWNER_SCRIPT: + return int(bool(await client.expire(lock_key, int(rest[0])))) + + return int(bool(await client.delete(lock_key))) + + lock_engine = engine.get_lock_engine() + + with ( + patch.object(lock_engine, "_client", return_value=client), + # `LockEngine.__getattr__` forwards to the client; a real attribute shadows it. + patch.object(lock_engine, "eval", _eval, create=True), + ): + yield client + + await client.aclose() + + +async def test_two_ordinary_projects_do_not_share_a_lock(fake_redis): + project_a, project_b = str(uuid4()), str(uuid4()) + + assert ( + await locking.acquire_lock(namespace="eval", key="run", project_id=project_a) + is not None + ) + assert ( + await locking.acquire_lock(namespace="eval", key="run", project_id=project_b) + is not None + ) + + +async def test_colliding_projects_still_share_a_lock_while_the_cover_lasts(fake_redis): + """A deliberate trade-off, confined to the lock namespace and to one release. + + Covering the previous release's key means two projects whose ids share a suffix keep + serializing against each other on locks. Dropping the cover instead would let pods on + either side of a rolling deploy into the same critical section, and losing mutual + exclusion is worse than two unrelated tenants queueing. Removing the transitional + cover (see `locking`) is what closes this last case. + + Cache keys are separated immediately either way — that is where the permission caches + live, and it is the part with a security consequence. + """ + assert _key(project_id=PROJECT_A) != _key(project_id=PROJECT_B) + + held = await locking.acquire_lock(namespace="eval", key="run", project_id=PROJECT_A) + assert held is not None + + assert ( + await locking.acquire_lock(namespace="eval", key="run", project_id=PROJECT_B) + is None + ) + + +async def test_a_lock_still_excludes_the_same_project(fake_redis): + held = await locking.acquire_lock(namespace="eval", key="run", project_id=PROJECT_A) + assert held is not None + + assert ( + await locking.acquire_lock(namespace="eval", key="run", project_id=PROJECT_A) + is None + ) + + await locking.release_lock( + namespace="eval", key="run", project_id=PROJECT_A, owner=held + ) + assert ( + await locking.acquire_lock(namespace="eval", key="run", project_id=PROJECT_A) + is not None + ) + + +async def test_a_holder_on_the_previous_release_still_blocks_this_one(fake_redis): + """The rolling-deploy case: an old pod holds only the truncated key. + + Without the transitional cover this is the window where two pods both believe they + hold the same logical lock. + """ + legacy_key = _pack( + namespace="lock:eval", + key="run", + project_id=PROJECT_A, + legacy_truncated_scope=True, + ) + # Exactly what a pod running the previous release writes. + await fake_redis.set(legacy_key, b"old-pod-owner", nx=True, ex=30) + + assert ( + await locking.acquire_lock(namespace="eval", key="run", project_id=PROJECT_A) + is None + ) + + +async def test_a_blocked_acquire_does_not_strand_the_legacy_key(fake_redis): + """Losing the race on the primary key must not leave the legacy one held. + + The legacy key is claimed first, so a caller that then loses the primary has to give + it back — otherwise a failed acquire would block the section for the full TTL. + """ + lock_key, legacy_key = locking._lock_keys( + namespace="eval", key="run", project_id=PROJECT_A + ) + assert legacy_key is not None + + # Someone already holds the primary; the caller below will claim the legacy key, + # fail on the primary, and must then release what it took. + await fake_redis.set(lock_key, b"another-owner", nx=True, ex=30) + + assert ( + await locking.acquire_lock(namespace="eval", key="run", project_id=PROJECT_A) + is None + ) + assert await fake_redis.get(legacy_key) is None + + +async def test_release_clears_both_generations(fake_redis): + lock_key, legacy_key = locking._lock_keys( + namespace="eval", key="run", project_id=PROJECT_A + ) + assert legacy_key is not None + + owner = await locking.acquire_lock( + namespace="eval", key="run", project_id=PROJECT_A + ) + assert owner is not None + assert await fake_redis.get(legacy_key) is not None + + await locking.release_lock( + namespace="eval", key="run", project_id=PROJECT_A, owner=owner + ) + + assert await fake_redis.get(lock_key) is None + assert await fake_redis.get(legacy_key) is None + + +async def test_a_short_scope_takes_only_one_key(fake_redis): + """When the two shapes coincide there is no second key — and no double SET NX. + + Claiming the same key twice with NX would fail the second call and make every + acquire in this shape look blocked. + """ + short_project = "abc" + lock_key, legacy_key = locking._lock_keys( + namespace="eval", key="run", project_id=short_project + ) + assert legacy_key is None + + assert ( + await locking.acquire_lock( + namespace="eval", key="run", project_id=short_project + ) + is not None + ) + assert await fake_redis.get(lock_key) is not None + + +async def test_renew_keeps_both_generations_alive(fake_redis): + lock_key, legacy_key = locking._lock_keys( + namespace="eval", key="run", project_id=PROJECT_A + ) + assert legacy_key is not None + + owner = await locking.acquire_lock( + namespace="eval", key="run", project_id=PROJECT_A, ttl=5 + ) + assert owner is not None + + assert await locking.renew_lock( + namespace="eval", key="run", project_id=PROJECT_A, ttl=90, owner=owner + ) + + # A legacy key left on the original TTL would lapse mid-section and let a pod on the + # previous release in. + assert await fake_redis.ttl(lock_key) > 5 + assert await fake_redis.ttl(legacy_key) > 5 From 1d6e9df4bd95f84a982121601978b2d5d1a4c47a Mon Sep 17 00:00:00 2001 From: WhoamiI00 Date: Tue, 25 Aug 2026 22:57:10 +0530 Subject: [PATCH 2/2] docs(pr): add before/after image for the cache key scope change --- .../6166-full-project-id-cache-keys.png | Bin 0 -> 35467 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 .github/pr-assets/6166-full-project-id-cache-keys.png diff --git a/.github/pr-assets/6166-full-project-id-cache-keys.png b/.github/pr-assets/6166-full-project-id-cache-keys.png new file mode 100644 index 0000000000000000000000000000000000000000..d20f47d2025120a393ab2973dbdfb8db455f3e63 GIT binary patch literal 35467 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