misaki/server/internal_session_test.go
q e8d6d80c0b
All checks were successful
Build & push Docker image / build (push) Successful in 1m51s
HMAC signing
2026-06-23 17:03:42 -04:00

162 lines
5.2 KiB
Go

package server_test
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"fmt"
"net/http"
"sort"
"strconv"
"strings"
"testing"
"time"
"misaki/db"
"misaki/server"
)
func sessionGroups(t *testing.T, srv http.Handler, d *db.DB, user, pass string) (int, []string) {
t.Helper()
cookie := doLogin(t, srv, user, pass)
rr := doGet(srv, "/internal/session-check", cookie)
raw := rr.Header().Get("X-Auth-Groups")
if raw == "" {
return rr.Code, nil
}
g := strings.Split(raw, ",")
sort.Strings(g)
return rr.Code, g
}
func eqGroups(a, b []string) bool {
if len(a) != len(b) {
return false
}
sort.Strings(b)
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
func TestSessionCheck_FilesGroups(t *testing.T) {
srv, d := newTestServer(t)
makeApprovedUser(t, d, "plain", "hunter2hunter2")
if code, g := sessionGroups(t, srv, d, "plain", "hunter2hunter2"); code != http.StatusOK || !eqGroups(g, []string{"users"}) {
t.Errorf("plain user: code=%d groups=%v, want 200 [users]", code, g)
}
id := makeApprovedUser(t, d, "dj", "hunter2hunter2")
if err := d.SetUserServiceEnabled(id, "files", true); err != nil {
t.Fatalf("grant files: %v", err)
}
if err := d.SetUserServiceData(id, "files", "share,fmradio"); err != nil {
t.Fatalf("set mounts: %v", err)
}
if code, g := sessionGroups(t, srv, d, "dj", "hunter2hunter2"); code != http.StatusOK || !eqGroups(g, []string{"users", "share", "fmradio"}) {
t.Errorf("files user: code=%d groups=%v, want [users share fmradio]", code, g)
}
id2 := makeApprovedUser(t, d, "star", "hunter2hunter2")
if err := d.SetUserServiceEnabled(id2, "files", true); err != nil {
t.Fatalf("grant files: %v", err)
}
if err := d.SetUserServiceData(id2, "files", "*"); err != nil {
t.Fatalf("set mounts: %v", err)
}
if code, g := sessionGroups(t, srv, d, "star", "hunter2hunter2"); code != http.StatusOK || !eqGroups(g, []string{"users", "admin"}) {
t.Errorf("wildcard files user: code=%d groups=%v, want [users admin]", code, g)
}
makeAdminUser(t, d, "boss", "hunter2hunter2")
if code, g := sessionGroups(t, srv, d, "boss", "hunter2hunter2"); code != http.StatusOK || !eqGroups(g, []string{"users", "admin"}) {
t.Errorf("admin user: code=%d groups=%v, want [users admin]", code, g)
}
}
func TestSessionCheck_SignedIdentity(t *testing.T) {
const secret = "test-auth-secret"
d := newTestDB(t)
srv := server.New(d, server.ServiceURLs{}, nil, server.SMTPConfig{},
server.Config{AuthSecret: secret})
makeApprovedUser(t, d, "alice", "hunter2hunter2")
cookie := doLogin(t, srv, "alice", "hunter2hunter2")
rr := doGet(srv, "/internal/session-check", cookie)
if rr.Code != http.StatusOK {
t.Fatalf("session-check: got %d, want 200", rr.Code)
}
sig := rr.Header().Get("X-Auth-Sig")
expRaw := rr.Header().Get("X-Auth-Exp")
if sig == "" || expRaw == "" {
t.Fatalf("missing signature headers: sig=%q exp=%q", sig, expRaw)
}
exp, err := strconv.ParseInt(expRaw, 10, 64)
if err != nil {
t.Fatalf("X-Auth-Exp not an int: %v", err)
}
if exp <= time.Now().Unix() {
t.Errorf("X-Auth-Exp %d is not in the future", exp)
}
mac := hmac.New(sha256.New, []byte(secret))
fmt.Fprintf(mac, "v1:%s:%d", "alice", exp)
want := base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
if sig != want {
t.Errorf("signature mismatch: got %q, want %q", sig, want)
}
mac2 := hmac.New(sha256.New, []byte(secret))
fmt.Fprintf(mac2, "v1:%s:%d", "admin", exp)
if forged := base64.RawURLEncoding.EncodeToString(mac2.Sum(nil)); forged == sig {
t.Error("signature collides for a different username")
}
}
func TestSessionCheck_NoSecretNoSignature(t *testing.T) {
srv, d := newTestServer(t)
makeApprovedUser(t, d, "bob", "hunter2hunter2")
cookie := doLogin(t, srv, "bob", "hunter2hunter2")
rr := doGet(srv, "/internal/session-check", cookie)
if rr.Code != http.StatusOK {
t.Fatalf("session-check: got %d, want 200", rr.Code)
}
if sig := rr.Header().Get("X-Auth-Sig"); sig != "" {
t.Errorf("unexpected signature when no secret configured: %q", sig)
}
}
func TestServicesAreIndependent(t *testing.T) {
const secret = "test-secret"
srv2, d := newSecretServer(t, secret)
id := makeApprovedUser(t, d, "onlyradio", "hunter2hunter2")
if err := d.SetUserServiceEnabled(id, "radio", true); err != nil {
t.Fatalf("grant radio: %v", err)
}
if err := d.SetUserServiceData(id, "radio", "fmradio"); err != nil {
t.Fatalf("set stations: %v", err)
}
if rr := streamAuth(srv2, secret, "onlyradio", "hunter2hunter2", "fmradio"); rr.Code != http.StatusOK {
t.Errorf("radio push: got %d, want 200", rr.Code)
}
if code, g := sessionGroups(t, srv2, d, "onlyradio", "hunter2hunter2"); code != http.StatusOK || !eqGroups(g, []string{"users"}) {
t.Errorf("radio user copyparty groups: code=%d groups=%v, want [users]", code, g)
}
id2 := makeApprovedUser(t, d, "onlyfiles", "hunter2hunter2")
if err := d.SetUserServiceEnabled(id2, "files", true); err != nil {
t.Fatalf("grant files: %v", err)
}
if err := d.SetUserServiceData(id2, "files", "fmradio"); err != nil {
t.Fatalf("set mounts: %v", err)
}
if rr := streamAuth(srv2, secret, "onlyfiles", "hunter2hunter2", "fmradio"); rr.Code != http.StatusForbidden {
t.Errorf("files user harbor push: got %d, want 403", rr.Code)
}
}