114 lines
4.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package router
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"hyapp-admin-server/internal/config"
"hyapp-admin-server/internal/modules/appuser"
"hyapp-admin-server/internal/modules/externaladmin"
"hyapp-admin-server/internal/modules/opscenter"
"hyapp-admin-server/internal/service"
"github.com/gin-gonic/gin"
)
func TestOpsCenterAppBootstrapDoesNotRequireAppCode(t *testing.T) {
gin.SetMode(gin.TestMode)
auth := service.NewAuthService("ops-center-router-test-secret", time.Hour)
token, _, err := auth.GenerateAccessToken(7, "operator", nil)
if err != nil {
t.Fatalf("generate access token: %v", err)
}
engine := New(config.Config{}, auth, nil, Handlers{OpsCenter: opscenter.New(nil, nil)})
request := httptest.NewRequest(http.MethodGet, "/api/v1/admin/ops-center/apps", nil)
request.Header.Set("Authorization", "Bearer "+token)
response := httptest.NewRecorder()
engine.ServeHTTP(response, request)
// Ops Center 先读取跨 App 目录,再按 app_code 查询每个应用的数据;启动接口若要求
// X-App-Code就会形成“先拿到 App 才能发 header、先发 header 才能拿 App”的死锁。
if response.Code != http.StatusOK {
t.Fatalf("status = %d body=%s", response.Code, response.Body.String())
}
if body := response.Body.String(); !strings.Contains(body, `"app_code":"yumi"`) || !strings.Contains(body, `"app_code":"aslan"`) {
t.Fatalf("bootstrap app catalog missing fixed apps: %s", body)
}
// 从 appProtected 移出只解除 App 选择依赖;路由仍必须位于 protected 下,不能变成匿名应用目录。
unauthorized := httptest.NewRecorder()
engine.ServeHTTP(unauthorized, httptest.NewRequest(http.MethodGet, "/api/v1/admin/ops-center/apps", nil))
if unauthorized.Code != http.StatusUnauthorized {
t.Fatalf("unauthorized status = %d body=%s", unauthorized.Code, unauthorized.Body.String())
}
}
func TestExternalPortalRegistersOnlyExplicitAppUserWhitelist(t *testing.T) {
gin.SetMode(gin.TestMode)
auth := service.NewAuthService("external-router-test-secret", time.Hour)
externalHandler := externaladmin.New(nil, nil, externaladmin.Config{}, nil)
engine := New(config.Config{}, auth, nil, Handlers{
ExternalAdmin: externalHandler,
AppUser: &appuser.Handler{},
})
// The listed route exists and is protected by the isolated opaque session middleware.
allowed := httptest.NewRecorder()
engine.ServeHTTP(allowed, httptest.NewRequest(http.MethodGet, "/api/v1/external/app/users", nil))
if allowed.Code != http.StatusUnauthorized {
t.Fatalf("whitelisted route status = %d body=%s", allowed.Code, allowed.Body.String())
}
team := httptest.NewRecorder()
engine.ServeHTTP(team, httptest.NewRequest(http.MethodGet, "/api/v1/external/admin/my-team/agencies", nil))
if team.Code != http.StatusUnauthorized {
t.Fatalf("my-team route must exist behind external session auth: status=%d body=%s", team.Code, team.Body.String())
}
for _, path := range []string{
"/api/v1/external/app/users/1/password",
"/api/v1/external/app/users/1/access-token",
"/api/v1/external/exports/app-users",
} {
request := httptest.NewRequest(http.MethodPost, path, nil)
response := httptest.NewRecorder()
engine.ServeHTTP(response, request)
if response.Code != http.StatusNotFound {
t.Fatalf("non-whitelisted route %s status = %d body=%s", path, response.Code, response.Body.String())
}
}
}
func TestRouterDoesNotTrustForwardedIPFromDirectClient(t *testing.T) {
gin.SetMode(gin.TestMode)
engine := New(config.Config{TrustedProxies: []string{"127.0.0.0/8", "::1"}}, nil, nil, Handlers{})
engine.GET("/__test_client_ip", func(c *gin.Context) {
c.String(http.StatusOK, c.ClientIP())
})
// A direct caller is outside the configured ingress hop. Its XFF value must be
// ignored, otherwise it can rotate a forged address to bypass external-login limits.
directRequest := httptest.NewRequest(http.MethodGet, "/__test_client_ip", nil)
directRequest.RemoteAddr = "198.51.100.24:43210"
directRequest.Header.Set("X-Forwarded-For", "203.0.113.99")
directResponse := httptest.NewRecorder()
engine.ServeHTTP(directResponse, directRequest)
if got := directResponse.Body.String(); got != "198.51.100.24" {
t.Fatalf("direct client IP = %q, want socket peer", got)
}
// The local Nginx hop is trusted, but Gin must stop at the right-most untrusted
// address. A prepended attacker-controlled value therefore cannot become identity.
proxiedRequest := httptest.NewRequest(http.MethodGet, "/__test_client_ip", nil)
proxiedRequest.RemoteAddr = "127.0.0.1:43210"
proxiedRequest.Header.Set("X-Forwarded-For", "203.0.113.99, 198.51.100.24")
proxiedResponse := httptest.NewRecorder()
engine.ServeHTTP(proxiedResponse, proxiedRequest)
if got := proxiedResponse.Body.String(); got != "198.51.100.24" {
t.Fatalf("proxied client IP = %q, want right-most untrusted hop", got)
}
}