Files
kandev/apps/backend/internal/backendapp/lifecycle_access_wiring_test.go
T
João Salavisa 481359ddb2 feat(auth): organization units, tenancy, roles and scopes for team access (#2919)
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-05 13:40:43 +00:00

96 lines
3.4 KiB
Go

package backendapp
import (
"context"
"testing"
"github.com/kandev/kandev/internal/agent/runtime/lifecycle"
"github.com/kandev/kandev/internal/authz"
)
// recordingAuthorizer records which authorizer the lifecycle manager reached
// for each kind of check.
type recordingAuthorizer struct{ calls []string }
func (r *recordingAuthorizer) AuthorizeSessionAccess(_ context.Context, sessionID string) error {
r.calls = append(r.calls, "session:"+sessionID)
return nil
}
func (r *recordingAuthorizer) AuthorizeEnvironmentAccess(_ context.Context, envID string) error {
r.calls = append(r.calls, "environment:"+envID)
return nil
}
func (r *recordingAuthorizer) AuthorizeTaskAccess(_ context.Context, taskID string) error {
r.calls = append(r.calls, "task:"+taskID)
return nil
}
func (r *recordingAuthorizer) AuthorizeTaskEnvironmentAccess(_ context.Context, taskID, envID string) error {
r.calls = append(r.calls, "pair:"+taskID+"/"+envID)
return nil
}
func (r *recordingAuthorizer) AuthorizeSessionScope(_ context.Context, sessionID string, scope authz.Scope) error {
r.calls = append(r.calls, "session-scope:"+sessionID+"/"+string(scope))
return nil
}
func (r *recordingAuthorizer) AuthorizeEnvironmentScope(_ context.Context, envID string, scope authz.Scope) error {
r.calls = append(r.calls, "environment-scope:"+envID+"/"+string(scope))
return nil
}
// TestWireLifecycleAccessCheckers pins the hop between the task service and the
// lifecycle manager. The three single-ID checkers share the signature
// `func(context.Context, string) error`, so wiring session visibility into the
// task slot compiles cleanly and authorizes the wrong resource for every
// caller downstream (the SSR terminal routes, the vscode and port proxies).
// Checking each check reaches its own authorizer is the only thing that catches
// a crossed or dropped wire.
func TestWireLifecycleAccessCheckers(t *testing.T) {
access := &recordingAuthorizer{}
mgr := &lifecycle.Manager{}
wireLifecycleAccessCheckers(mgr, access)
ctx := context.Background()
if err := mgr.CheckSessionAccess(ctx, "sess-1"); err != nil {
t.Fatalf("CheckSessionAccess: %v", err)
}
if err := mgr.CheckEnvironmentAccess(ctx, "env-1"); err != nil {
t.Fatalf("CheckEnvironmentAccess: %v", err)
}
if err := mgr.CheckTaskAccess(ctx, "task-1"); err != nil {
t.Fatalf("CheckTaskAccess: %v", err)
}
if err := mgr.CheckTaskEnvironmentAccess(ctx, "task-1", "env-1"); err != nil {
t.Fatalf("CheckTaskEnvironmentAccess: %v", err)
}
// The exec checkers fall back to the read check when unwired, so a dropped
// wire here is silent: the surface still authorizes, just at the wrong
// scope. Asserting the scope-carrying authorizer is the one reached (and
// with session.exec, not a read scope) is what catches that.
if err := mgr.CheckSessionExecAccess(ctx, "sess-1"); err != nil {
t.Fatalf("CheckSessionExecAccess: %v", err)
}
if err := mgr.CheckEnvironmentExecAccess(ctx, "env-1"); err != nil {
t.Fatalf("CheckEnvironmentExecAccess: %v", err)
}
want := []string{
"session:sess-1", "environment:env-1", "task:task-1", "pair:task-1/env-1",
"session-scope:sess-1/" + string(authz.ScopeSessionExec),
"environment-scope:env-1/" + string(authz.ScopeSessionExec),
}
if len(access.calls) != len(want) {
t.Fatalf("calls = %v, want %v", access.calls, want)
}
for i, call := range want {
if access.calls[i] != call {
t.Errorf("call %d = %q, want %q (crossed wire)", i, access.calls[i], call)
}
}
}