Mitchell, on the read-only version built earlier today: scratch it. He hands these to people he trusts and revokes them otherwise, so a console with half its tabs missing solved a problem he doesn't have. So the allow-list, the middleware check and every "not for a guest" branch in the UI are gone. What is left is the part that was the point: a throwaway account that expires by itself, is checked on every request, swept once a minute, and can be ended in one tap. A guest now gets the whole console on every machine, terminal included. Said once, since it is his call and he has made it: that means a guest has a root-capable shell on every host, and can make an account or a token of its own that outlives the guest login. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
71 lines
2.1 KiB
Go
71 lines
2.1 KiB
Go
package api
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"hamlab/internal/auth"
|
|
"hamlab/internal/config"
|
|
"hamlab/internal/store"
|
|
)
|
|
|
|
// A guest login is an ordinary account with a clock on it, so the clock is the thing worth
|
|
// testing: one whose time is up is stopped at the door, whether or not the minute-by-minute
|
|
// sweep has reached their account yet, and their session is dropped on the way — so the
|
|
// console they left open goes back to the login form rather than carrying on.
|
|
func TestExpiredGuestIsRefusedAndSignedOut(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
expires time.Time
|
|
wantOK bool
|
|
}{
|
|
{"still has time", time.Now().Add(10 * time.Minute), true},
|
|
{"time is up", time.Now().Add(-time.Second), false},
|
|
{"no expiry at all", time.Time{}, false},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
cfg := config.Default(filepath.Join(dir, "config.json"))
|
|
if err := cfg.Update(func(c *config.Config) {
|
|
c.Users = []config.User{{Name: "guest-abcd", Hash: "x", Guest: true, Expires: tc.expires}}
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
st, err := store.Open(filepath.Join(dir, "state.json"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s := &Server{cfg: cfg, store: st, sessions: auth.NewSessions(st)}
|
|
tok, err := s.sessions.Create("guest-abcd", "10.0.0.5", "curl")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
r := httptest.NewRequest("GET", "/api/me", nil)
|
|
r.AddCookie(&http.Cookie{Name: cookieName, Value: tok})
|
|
w := httptest.NewRecorder()
|
|
a, ok, answered := s.cookieActor(w, r)
|
|
if ok != tc.wantOK {
|
|
t.Fatalf("ok = %v, want %v", ok, tc.wantOK)
|
|
}
|
|
if tc.wantOK {
|
|
if !a.Guest || a.User != "guest-abcd" {
|
|
t.Errorf("actor = %+v, want the guest account marked as one", a)
|
|
}
|
|
if answered {
|
|
t.Error("answered the caller for a guest that still has time")
|
|
}
|
|
return
|
|
}
|
|
if !answered || w.Code != 401 {
|
|
t.Errorf("answered = %v, code = %d, want true and 401", answered, w.Code)
|
|
}
|
|
if _, live := s.sessions.Lookup(tok, "10.0.0.5"); live {
|
|
t.Error("the session of an expired guest is still live")
|
|
}
|
|
})
|
|
}
|
|
}
|