mirror of
https://github.com/EZ-Api/ez-api.git
synced 2026-01-13 17:47:51 +00:00
Safeguard integer parsing in the `Whoami` handler by trimming whitespace and handling errors explicitly for `master_id`, `issued_at_epoch`, and `expires_at`. This prevents potential validation bypasses or incorrect behavior due to malformed metadata. Add unit tests to verify invalid epoch handling and response correctness.
155 lines
4.5 KiB
Go
155 lines
4.5 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/alicebob/miniredis/v2"
|
|
"github.com/ez-api/ez-api/internal/model"
|
|
"github.com/ez-api/ez-api/internal/service"
|
|
"github.com/ez-api/foundation/tokenhash"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/redis/go-redis/v9"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
func newAuthHandler(t *testing.T) (*AuthHandler, *gorm.DB, *miniredis.Miniredis) {
|
|
t.Helper()
|
|
gin.SetMode(gin.TestMode)
|
|
t.Setenv("EZ_ADMIN_TOKEN", "admin-secret")
|
|
|
|
adminService, err := service.NewAdminService()
|
|
if err != nil {
|
|
t.Fatalf("NewAdminService: %v", err)
|
|
}
|
|
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
|
if err != nil {
|
|
t.Fatalf("open sqlite: %v", err)
|
|
}
|
|
if err := db.AutoMigrate(&model.Master{}, &model.Key{}); err != nil {
|
|
t.Fatalf("migrate: %v", err)
|
|
}
|
|
|
|
mr := miniredis.RunT(t)
|
|
rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()})
|
|
|
|
masterService := service.NewMasterService(db)
|
|
handler := NewAuthHandler(db, rdb, adminService, masterService)
|
|
return handler, db, mr
|
|
}
|
|
|
|
func TestAuthHandler_Whoami_InvalidIssuedAtEpoch_Returns401(t *testing.T) {
|
|
handler, _, mr := newAuthHandler(t)
|
|
|
|
token := "sk-live-invalid-epoch"
|
|
hash := tokenhash.HashToken(token)
|
|
mr.HSet("auth:token:"+hash, "master_id", "1")
|
|
mr.HSet("auth:token:"+hash, "issued_at_epoch", "bad")
|
|
mr.HSet("auth:token:"+hash, "status", "active")
|
|
mr.HSet("auth:token:"+hash, "group", "default")
|
|
mr.HSet("auth:master:1", "epoch", "1")
|
|
mr.HSet("auth:master:1", "status", "active")
|
|
|
|
r := gin.New()
|
|
r.GET("/auth/whoami", handler.Whoami)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/auth/whoami", nil)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
rr := httptest.NewRecorder()
|
|
r.ServeHTTP(rr, req)
|
|
|
|
if rr.Code != http.StatusUnauthorized {
|
|
t.Fatalf("expected 401, got %d body=%s", rr.Code, rr.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestAuthHandler_Whoami_InvalidMasterEpoch_Returns401(t *testing.T) {
|
|
handler, _, mr := newAuthHandler(t)
|
|
|
|
token := "sk-live-invalid-master-epoch"
|
|
hash := tokenhash.HashToken(token)
|
|
mr.HSet("auth:token:"+hash, "master_id", "1")
|
|
mr.HSet("auth:token:"+hash, "issued_at_epoch", "1")
|
|
mr.HSet("auth:token:"+hash, "status", "active")
|
|
mr.HSet("auth:token:"+hash, "group", "default")
|
|
mr.HSet("auth:master:1", "epoch", "bad")
|
|
mr.HSet("auth:master:1", "status", "active")
|
|
|
|
r := gin.New()
|
|
r.GET("/auth/whoami", handler.Whoami)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/auth/whoami", nil)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
rr := httptest.NewRecorder()
|
|
r.ServeHTTP(rr, req)
|
|
|
|
if rr.Code != http.StatusUnauthorized {
|
|
t.Fatalf("expected 401, got %d body=%s", rr.Code, rr.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestAuthHandler_Whoami_KeyResponseIncludesIPRules(t *testing.T) {
|
|
handler, db, mr := newAuthHandler(t)
|
|
|
|
token := "sk-live-valid"
|
|
hash := tokenhash.HashToken(token)
|
|
expiresAt := time.Now().Add(time.Hour).Unix()
|
|
|
|
mr.HSet("auth:token:"+hash, "master_id", "1")
|
|
mr.HSet("auth:token:"+hash, "issued_at_epoch", "1")
|
|
mr.HSet("auth:token:"+hash, "status", "active")
|
|
mr.HSet("auth:token:"+hash, "group", "default")
|
|
mr.HSet("auth:token:"+hash, "expires_at", fmt.Sprintf("%d", expiresAt))
|
|
mr.HSet("auth:master:1", "epoch", "1")
|
|
mr.HSet("auth:master:1", "status", "active")
|
|
|
|
key := &model.Key{
|
|
MasterID: 1,
|
|
TokenHash: hash,
|
|
Group: "default",
|
|
Scopes: "chat:write",
|
|
DefaultNamespace: "default",
|
|
Namespaces: "default",
|
|
Status: "active",
|
|
IssuedAtEpoch: 1,
|
|
IssuedBy: "master",
|
|
AllowIPs: "1.2.3.4",
|
|
DenyIPs: "5.6.7.0/24",
|
|
}
|
|
if err := db.Create(key).Error; err != nil {
|
|
t.Fatalf("create key: %v", err)
|
|
}
|
|
|
|
r := gin.New()
|
|
r.GET("/auth/whoami", handler.Whoami)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/auth/whoami", nil)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
rr := httptest.NewRecorder()
|
|
r.ServeHTTP(rr, req)
|
|
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d body=%s", rr.Code, rr.Body.String())
|
|
}
|
|
|
|
var resp map[string]any
|
|
if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if resp["allow_ips"] != "1.2.3.4" {
|
|
t.Fatalf("expected allow_ips, got %v", resp["allow_ips"])
|
|
}
|
|
if resp["deny_ips"] != "5.6.7.0/24" {
|
|
t.Fatalf("expected deny_ips, got %v", resp["deny_ips"])
|
|
}
|
|
if got, ok := resp["expires_at"].(float64); !ok || int64(got) != expiresAt {
|
|
t.Fatalf("expected expires_at=%d, got %v", expiresAt, resp["expires_at"])
|
|
}
|
|
}
|