114 lines
4.8 KiB
Go
114 lines
4.8 KiB
Go
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)
|
||
}
|
||
}
|