mirror of
https://github.com/Noooste/garage-ui.git
synced 2026-08-06 20:07:40 +00:00
9022b90f02
Signed-off-by: Noooste <83548733+Noooste@users.noreply.github.com>
404 lines
11 KiB
Go
404 lines
11 KiB
Go
package middleware
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"Noooste/garage-ui/internal/auth"
|
|
"Noooste/garage-ui/internal/config"
|
|
logpkg "Noooste/garage-ui/pkg/logger"
|
|
|
|
"github.com/gofiber/fiber/v3"
|
|
"github.com/rs/zerolog"
|
|
)
|
|
|
|
// newAuthTestApp builds a fiber.App with RequestID + Logging (buffer-backed) +
|
|
// AuthMiddleware + a trivial /protected handler that echoes username/auth
|
|
// method into the JSON body so tests can assert on locals.
|
|
func newAuthTestApp(t *testing.T, buf *bytes.Buffer, authCfg *config.AuthConfig, svc *auth.Service) *fiber.App {
|
|
t.Helper()
|
|
base := zerolog.New(buf)
|
|
app := fiber.New()
|
|
app.Use(RequestID())
|
|
app.Use(Logging(base))
|
|
app.Use(AuthMiddleware(authCfg, svc))
|
|
app.Get("/protected", func(c fiber.Ctx) error {
|
|
uname, _ := c.Locals("username").(string)
|
|
email, _ := c.Locals("email").(string)
|
|
logpkg.FromCtx(c.Context()).Info().Msg("in_handler")
|
|
return c.JSON(fiber.Map{
|
|
"ok": true,
|
|
"username": uname,
|
|
"email": email,
|
|
})
|
|
})
|
|
return app
|
|
}
|
|
|
|
// newAuthSvc returns an *auth.Service with the given auth config, JWT
|
|
// service initialized, OIDC disabled unless the caller wires it.
|
|
func newAuthSvc(t *testing.T, authCfg *config.AuthConfig) *auth.Service {
|
|
t.Helper()
|
|
svc, err := auth.NewAuthService(authCfg, &config.ServerConfig{})
|
|
if err != nil {
|
|
t.Fatalf("NewAuthService: %v", err)
|
|
}
|
|
return svc
|
|
}
|
|
|
|
// findLine returns the first parsed log line whose "message" field equals msg,
|
|
// failing the test if no such line is present.
|
|
func findLine(t *testing.T, buf *bytes.Buffer, msg string) map[string]any {
|
|
t.Helper()
|
|
for _, line := range parseLines(t, buf) {
|
|
if line["message"] == msg {
|
|
return line
|
|
}
|
|
}
|
|
t.Fatalf("no %q log line: %s", msg, buf.String())
|
|
return nil
|
|
}
|
|
|
|
func TestAuthMiddleware_BothDisabled_AllowsRequest(t *testing.T) {
|
|
authCfg := &config.AuthConfig{
|
|
Admin: config.AdminAuthConfig{Enabled: false},
|
|
OIDC: config.OIDCConfig{Enabled: false},
|
|
}
|
|
svc := newAuthSvc(t, authCfg)
|
|
|
|
var buf bytes.Buffer
|
|
app := newAuthTestApp(t, &buf, authCfg, svc)
|
|
|
|
req := httptest.NewRequest("GET", "/protected", nil)
|
|
resp, err := app.Test(req)
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
if resp.StatusCode != 200 {
|
|
t.Fatalf("status = %d, want 200", resp.StatusCode)
|
|
}
|
|
var body map[string]any
|
|
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if body["ok"] != true {
|
|
t.Errorf("ok = %v, want true", body["ok"])
|
|
}
|
|
if body["username"] != "" {
|
|
t.Errorf("username should be empty when auth disabled, got %q", body["username"])
|
|
}
|
|
}
|
|
|
|
func newAdminCfg() *config.AuthConfig {
|
|
return &config.AuthConfig{
|
|
Admin: config.AdminAuthConfig{Enabled: true, Username: "admin", Password: "pw"},
|
|
OIDC: config.OIDCConfig{Enabled: false},
|
|
}
|
|
}
|
|
|
|
func TestAuthMiddleware_Admin_BearerValid_AllowsAndEnrichesLogger(t *testing.T) {
|
|
authCfg := newAdminCfg()
|
|
svc := newAuthSvc(t, authCfg)
|
|
tok, err := svc.GenerateSessionToken(&auth.UserInfo{Username: "admin", Email: "a@b"})
|
|
if err != nil {
|
|
t.Fatalf("GenerateSessionToken: %v", err)
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
app := newAuthTestApp(t, &buf, authCfg, svc)
|
|
|
|
req := httptest.NewRequest("GET", "/protected", nil)
|
|
req.Header.Set("Authorization", "Bearer "+tok)
|
|
resp, err := app.Test(req)
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
if resp.StatusCode != 200 {
|
|
t.Fatalf("status = %d, want 200", resp.StatusCode)
|
|
}
|
|
var body map[string]any
|
|
_ = json.NewDecoder(resp.Body).Decode(&body)
|
|
if body["username"] != "admin" {
|
|
t.Errorf("username = %v, want admin", body["username"])
|
|
}
|
|
if body["email"] != "a@b" {
|
|
t.Errorf("email = %v, want a@b", body["email"])
|
|
}
|
|
|
|
// Enriched handler log line should carry user_id and auth_method=admin.
|
|
access := findLine(t, &buf, "in_handler")
|
|
if access["user_id"] != "admin" {
|
|
t.Errorf("user_id = %v, want admin", access["user_id"])
|
|
}
|
|
if access["auth_method"] != "admin" {
|
|
t.Errorf("auth_method = %v, want admin", access["auth_method"])
|
|
}
|
|
}
|
|
|
|
func TestAuthMiddleware_Admin_BearerInvalid_Returns401(t *testing.T) {
|
|
authCfg := newAdminCfg()
|
|
svc := newAuthSvc(t, authCfg)
|
|
|
|
var buf bytes.Buffer
|
|
app := newAuthTestApp(t, &buf, authCfg, svc)
|
|
|
|
req := httptest.NewRequest("GET", "/protected", nil)
|
|
req.Header.Set("Authorization", "Bearer not-a-real-token")
|
|
resp, err := app.Test(req)
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
if resp.StatusCode != 401 {
|
|
t.Fatalf("status = %d, want 401", resp.StatusCode)
|
|
}
|
|
var env struct {
|
|
Success bool `json:"success"`
|
|
Error struct {
|
|
Code string `json:"code"`
|
|
} `json:"error"`
|
|
}
|
|
_ = json.NewDecoder(resp.Body).Decode(&env)
|
|
if env.Success {
|
|
t.Error("success should be false")
|
|
}
|
|
if env.Error.Code != "UNAUTHORIZED" {
|
|
t.Errorf("error.code = %q, want UNAUTHORIZED", env.Error.Code)
|
|
}
|
|
|
|
// Warn log should carry reason=no_valid_credentials without leaking the token.
|
|
warn := findLine(t, &buf, "authentication_failed")
|
|
if warn["reason"] != "no_valid_credentials" {
|
|
t.Errorf("reason = %v", warn["reason"])
|
|
}
|
|
if warn["level"] != "warn" {
|
|
t.Errorf("level = %v, want warn", warn["level"])
|
|
}
|
|
if strings.Contains(buf.String(), "not-a-real-token") {
|
|
t.Error("token value must not appear in logs")
|
|
}
|
|
}
|
|
|
|
func TestAuthMiddleware_Admin_NoAuthHeader_Returns401(t *testing.T) {
|
|
authCfg := newAdminCfg()
|
|
svc := newAuthSvc(t, authCfg)
|
|
|
|
var buf bytes.Buffer
|
|
app := newAuthTestApp(t, &buf, authCfg, svc)
|
|
|
|
req := httptest.NewRequest("GET", "/protected", nil)
|
|
resp, err := app.Test(req)
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
if resp.StatusCode != 401 {
|
|
t.Fatalf("status = %d, want 401", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestAuthMiddleware_Admin_NonBearerScheme_Returns401(t *testing.T) {
|
|
authCfg := newAdminCfg()
|
|
svc := newAuthSvc(t, authCfg)
|
|
|
|
var buf bytes.Buffer
|
|
app := newAuthTestApp(t, &buf, authCfg, svc)
|
|
|
|
req := httptest.NewRequest("GET", "/protected", nil)
|
|
req.Header.Set("Authorization", "Basic dXNlcjpwdw==")
|
|
resp, err := app.Test(req)
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
if resp.StatusCode != 401 {
|
|
t.Fatalf("status = %d, want 401", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func newOIDCCfg(cookieName string) *config.AuthConfig {
|
|
return &config.AuthConfig{
|
|
Admin: config.AdminAuthConfig{Enabled: false},
|
|
OIDC: config.OIDCConfig{
|
|
Enabled: true,
|
|
CookieName: cookieName,
|
|
},
|
|
}
|
|
}
|
|
|
|
// newOIDCSvc returns a Service whose OIDC is *not* initialized (no dialing
|
|
// needed), which is fine because AuthMiddleware's OIDC branch only calls
|
|
// ValidateSessionToken — a pure JWT operation.
|
|
func newOIDCSvc(t *testing.T) *auth.Service {
|
|
t.Helper()
|
|
return newAuthSvc(t, &config.AuthConfig{OIDC: config.OIDCConfig{Enabled: false}})
|
|
}
|
|
|
|
func TestAuthMiddleware_OIDC_ValidCookie_Allows(t *testing.T) {
|
|
cfg := newOIDCCfg("session")
|
|
svc := newOIDCSvc(t)
|
|
|
|
tok, err := svc.GenerateSessionToken(&auth.UserInfo{Username: "alice", Email: "a@x"})
|
|
if err != nil {
|
|
t.Fatalf("GenerateSessionToken: %v", err)
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
app := newAuthTestApp(t, &buf, cfg, svc)
|
|
|
|
req := httptest.NewRequest("GET", "/protected", nil)
|
|
req.AddCookie(&http.Cookie{Name: "session", Value: tok})
|
|
resp, err := app.Test(req)
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
if resp.StatusCode != 200 {
|
|
t.Fatalf("status = %d, want 200", resp.StatusCode)
|
|
}
|
|
|
|
access := findLine(t, &buf, "in_handler")
|
|
if access["auth_method"] != "oidc" {
|
|
t.Errorf("auth_method = %v, want oidc", access["auth_method"])
|
|
}
|
|
if access["user_id"] != "alice" {
|
|
t.Errorf("user_id = %v, want alice", access["user_id"])
|
|
}
|
|
}
|
|
|
|
func TestAuthMiddleware_OIDC_InvalidCookie_Returns401(t *testing.T) {
|
|
cfg := newOIDCCfg("session")
|
|
svc := newOIDCSvc(t)
|
|
|
|
var buf bytes.Buffer
|
|
app := newAuthTestApp(t, &buf, cfg, svc)
|
|
|
|
req := httptest.NewRequest("GET", "/protected", nil)
|
|
req.AddCookie(&http.Cookie{Name: "session", Value: "garbage"})
|
|
resp, err := app.Test(req)
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
if resp.StatusCode != 401 {
|
|
t.Fatalf("status = %d, want 401", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestAuthMiddleware_OIDC_NoCookie_Returns401(t *testing.T) {
|
|
cfg := newOIDCCfg("session")
|
|
svc := newOIDCSvc(t)
|
|
|
|
var buf bytes.Buffer
|
|
app := newAuthTestApp(t, &buf, cfg, svc)
|
|
|
|
req := httptest.NewRequest("GET", "/protected", nil)
|
|
resp, err := app.Test(req)
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
if resp.StatusCode != 401 {
|
|
t.Fatalf("status = %d, want 401", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func newBothCfg(cookieName string) *config.AuthConfig {
|
|
return &config.AuthConfig{
|
|
Admin: config.AdminAuthConfig{Enabled: true, Username: "admin", Password: "pw"},
|
|
OIDC: config.OIDCConfig{
|
|
Enabled: true,
|
|
CookieName: cookieName,
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestAuthMiddleware_Both_BearerValid_AdminPathWins(t *testing.T) {
|
|
cfg := newBothCfg("session")
|
|
// OIDC disabled on the Service is fine — see Task 3 rationale.
|
|
svc := newAuthSvc(t, &config.AuthConfig{Admin: cfg.Admin})
|
|
|
|
tok, err := svc.GenerateSessionToken(&auth.UserInfo{Username: "admin"})
|
|
if err != nil {
|
|
t.Fatalf("GenerateSessionToken: %v", err)
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
app := newAuthTestApp(t, &buf, cfg, svc)
|
|
|
|
req := httptest.NewRequest("GET", "/protected", nil)
|
|
req.Header.Set("Authorization", "Bearer "+tok)
|
|
// Also set a valid OIDC cookie; admin should still win.
|
|
cookieTok, _ := svc.GenerateSessionToken(&auth.UserInfo{Username: "alice"})
|
|
req.AddCookie(&http.Cookie{Name: "session", Value: cookieTok})
|
|
|
|
resp, err := app.Test(req)
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
if resp.StatusCode != 200 {
|
|
t.Fatalf("status = %d, want 200", resp.StatusCode)
|
|
}
|
|
|
|
access := findLine(t, &buf, "in_handler")
|
|
if access["auth_method"] != "admin" {
|
|
t.Errorf("auth_method = %v, want admin", access["auth_method"])
|
|
}
|
|
if access["user_id"] != "admin" {
|
|
t.Errorf("user_id = %v, want admin", access["user_id"])
|
|
}
|
|
}
|
|
|
|
func TestAuthMiddleware_Both_BearerInvalid_FallsThroughToOIDCCookie(t *testing.T) {
|
|
cfg := newBothCfg("session")
|
|
svc := newAuthSvc(t, &config.AuthConfig{Admin: cfg.Admin})
|
|
|
|
cookieTok, err := svc.GenerateSessionToken(&auth.UserInfo{Username: "alice"})
|
|
if err != nil {
|
|
t.Fatalf("GenerateSessionToken: %v", err)
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
app := newAuthTestApp(t, &buf, cfg, svc)
|
|
|
|
req := httptest.NewRequest("GET", "/protected", nil)
|
|
req.Header.Set("Authorization", "Bearer bogus")
|
|
req.AddCookie(&http.Cookie{Name: "session", Value: cookieTok})
|
|
|
|
resp, err := app.Test(req)
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
if resp.StatusCode != 200 {
|
|
t.Fatalf("status = %d, want 200 (OIDC fallback)", resp.StatusCode)
|
|
}
|
|
|
|
access := findLine(t, &buf, "in_handler")
|
|
if access["auth_method"] != "oidc" {
|
|
t.Errorf("auth_method = %v, want oidc", access["auth_method"])
|
|
}
|
|
}
|
|
|
|
func TestAuthMiddleware_Both_AllInvalid_Returns401WithCombinedMethodLabel(t *testing.T) {
|
|
cfg := newBothCfg("session")
|
|
svc := newAuthSvc(t, &config.AuthConfig{Admin: cfg.Admin})
|
|
|
|
var buf bytes.Buffer
|
|
app := newAuthTestApp(t, &buf, cfg, svc)
|
|
|
|
req := httptest.NewRequest("GET", "/protected", nil)
|
|
req.Header.Set("Authorization", "Bearer bogus")
|
|
req.AddCookie(&http.Cookie{Name: "session", Value: "also-bogus"})
|
|
|
|
resp, err := app.Test(req)
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
if resp.StatusCode != 401 {
|
|
t.Fatalf("status = %d, want 401", resp.StatusCode)
|
|
}
|
|
|
|
warn := findLine(t, &buf, "authentication_failed")
|
|
if warn["auth_method"] != "admin+oidc" {
|
|
t.Errorf("auth_method = %v, want admin+oidc", warn["auth_method"])
|
|
}
|
|
}
|