Problem Statement
#6745 made namespace scope a first-class, required value at the store seam: every
namespace-bound operation takes a scope.Scope, and deliberately crossing namespaces requires the
scope.NewUnbounded constructor with a written reason.
Four operations were deferred by that change rather than bounded, because their call chains carry no
namespace anywhere — bounding them changes the SSH-facing contract, which deserves its own change:
SessionService.DeactivateSession
SessionService.KeepAliveSession
SessionService.UpdateSession
SessionService.EventSession
All four resolve a session by UID alone. They currently carry an unbounded scope with the shared
reason constant reasonInternalSessionMutation (api/services/scope.go), so they are visible in the
audit grep, but nothing checks that the session they mutate belongs to the caller.
The cloud edition has the same shape in its session-recording reads
(cloud/internal/cloud/services/session.go, reason reasonSessionRecordingByUID): the console route
and the background asciinema task both resolve a session by UID with no namespace in the chain.
Why it was deferred
The SSH server drives these through /internal/sessions/*, and the session UID is the only
identifier it holds at that point. Threading a namespace down means changing the internal HTTP
contract and the SSH server's session bookkeeping — a wider blast radius than the store-seam sweep,
and one that wants its own testing.
Suggested approach
- Decide where the namespace enters: the session UID could be resolved to its namespace once at the
edge, or the SSH server could carry the tenant it already authenticated the device against.
- Bound the four service methods (and the two cloud recording reads) to it.
- Drop
reasonInternalSessionMutation and reasonSessionRecordingByUID once nothing uses them.
Acceptance
grep -rn "scope.NewUnbounded(" --include="*.go" no longer lists the internal session mutations or
the cloud recording reads.
- The store isolation suite gains a cross-namespace negative test for each newly-bounded operation,
in the same shape as api/store/storetest/scope_isolation_tests.go.
Problem Statement
#6745 made namespace scope a first-class, required value at the store seam: every
namespace-bound operation takes a
scope.Scope, and deliberately crossing namespaces requires thescope.NewUnboundedconstructor with a written reason.Four operations were deferred by that change rather than bounded, because their call chains carry no
namespace anywhere — bounding them changes the SSH-facing contract, which deserves its own change:
SessionService.DeactivateSessionSessionService.KeepAliveSessionSessionService.UpdateSessionSessionService.EventSessionAll four resolve a session by UID alone. They currently carry an unbounded scope with the shared
reason constant
reasonInternalSessionMutation(api/services/scope.go), so they are visible in theaudit grep, but nothing checks that the session they mutate belongs to the caller.
The cloud edition has the same shape in its session-recording reads
(
cloud/internal/cloud/services/session.go, reasonreasonSessionRecordingByUID): the console routeand the background asciinema task both resolve a session by UID with no namespace in the chain.
Why it was deferred
The SSH server drives these through
/internal/sessions/*, and the session UID is the onlyidentifier it holds at that point. Threading a namespace down means changing the internal HTTP
contract and the SSH server's session bookkeeping — a wider blast radius than the store-seam sweep,
and one that wants its own testing.
Suggested approach
edge, or the SSH server could carry the tenant it already authenticated the device against.
reasonInternalSessionMutationandreasonSessionRecordingByUIDonce nothing uses them.Acceptance
grep -rn "scope.NewUnbounded(" --include="*.go"no longer lists the internal session mutations orthe cloud recording reads.
in the same shape as
api/store/storetest/scope_isolation_tests.go.