package main
import (
"net/http"
"net/http/httptest"
"testing"
"craftdesign.group/quarry/internal/api"
"craftdesign.group/quarry/internal/oltp"
"craftdesign.group/quarry/internal/ops"
"craftdesign.group/quarry/internal/web"
)
// A SESSION OPENED BY A SIGN-IN LINK CARRIES ITS TOKEN'S CEILING TO THE
// PAGES, A PASSWORD SESSION CARRIES NO TOKEN, AND REVOKING THE TOKEN
// TAKES THE SESSION AWAY (RFC 0021, Web sign-in).
func TestALinkSessionIsCutToItsTokensCeiling(t *testing.T) {
g := newDoorRig(t)
db := g.rg.odb
_, tok := g.mint(t, oltp.TokenSpec{Scopes: scopes(oltp.ScopeWeb), Ceiling: "o"})
link, _, err := db.MintSigninLink("jan", tok.ID, "/")
if err != nil {
t.Fatal(err)
}
cookie, _, _, ok := db.RedeemSigninLink(link, "", "")
if !ok {
t.Fatal("FAIL: the link opened no session")
}
pwCookie, _, ok := db.NewSession("jan", doorPassword)
if !ok {
t.Fatal("FAIL: password sign-in")
}
// What the pages see: the token on the request context, if any.
seen := func(cookieValue string) (hasToken bool, canO, canI bool) {
var out struct{ has, o, i bool }
h := g.doors.wrap(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if tk, ok := ops.TokenOf(req.Context()); ok {
out.has = true
out.o = tk.Caps("vault").HasAny("o")
out.i = tk.Caps("vault").HasAny("i")
}
}))
req := httptest.NewRequest(http.MethodGet, "/r/vault", nil)
req.AddCookie(&http.Cookie{Name: web.SessionCookie, Value: cookieValue})
h.ServeHTTP(httptest.NewRecorder(), req)
return out.has, out.o, out.i
}
if has, o, i := seen(cookie); !has || !o || i {
t.Errorf("FAIL: link session token=%v o=%v i=%v; want a token holding o and not i (the account holds both)", has, o, i)
}
if has, _, _ := seen(pwCookie); has {
t.Errorf("FAIL: a password session was given a token")
}
if err := db.RevokeToken("jan", tok.ID); err != nil {
t.Fatal(err)
}
if has, _, _ := seen(cookie); has {
t.Errorf("FAIL: the revoked token's session still carries it")
}
if _, ok := db.SessionAccount(cookie); ok {
t.Errorf("FAIL: the revoked token's session still resolves")
}
}
// THROUGH THE REAL REQUEST PATH, A LINK SESSION CEILINGED TO o IS REFUSED
// AT A SITE-ADMIN PAGE WHERE THE SAME ACCOUNT'S PASSWORD SESSION IS NOT.
func TestLinkSessionIsRefusedAtASiteAdminPage(t *testing.T) {
g := newDoorRig(t)
db := g.rg.odb
if err := db.AddAccount("boss", doorPassword, true); err != nil {
t.Fatal(err)
}
_, tok, err := db.MintToken("boss", oltp.TokenSpec{Name: "t", Scopes: scopes(oltp.ScopeWeb), Ceiling: "o"})
if err != nil {
t.Fatal(err)
}
link, _, err := db.MintSigninLink("boss", tok.ID, "/")
if err != nil {
t.Fatal(err)
}
linkCookie, _, _, ok := db.RedeemSigninLink(link, "", "")
if !ok {
t.Fatal("FAIL: the link opened no session")
}
pwCookie, _, ok := db.NewSession("boss", doorPassword)
if !ok {
t.Fatal("FAIL: password sign-in")
}
r := ops.NewRegistry()
if err := api.RegisterAll(r, api.Deps{Version: "v", Accounts: accountStore{db: db}, Repos: api.NewRepos(map[string]*api.Repo{})}); err != nil {
t.Fatal(err)
}
deny := func(string, string, string, ops.Auth) api.Decision { return api.Denied }
h := g.doors.wrap(web.HandlerWith(r, nil, nil,
[]web.Option{web.WithSessions(sessionPlane{db: db}), web.WithCaps(capsPlane{db: db})}, web.Verify(deny)))
get := func(cookie string) int {
req := httptest.NewRequest(http.MethodGet, "/fleet/people", nil)
req.AddCookie(&http.Cookie{Name: web.SessionCookie, Value: cookie})
w := httptest.NewRecorder()
h.ServeHTTP(w, req)
return w.Code
}
if code := get(pwCookie); code != http.StatusOK {
t.Fatalf("FAIL: a site admin's password session at /fleet/people: %d; want 200", code)
}
if code := get(linkCookie); code != http.StatusForbidden {
t.Errorf("FAIL: a link session ceilinged to o at /fleet/people: %d; want 403", code)
}
}