test: 覆盖管理端 HTTP 凭据边界 (#23)
This commit is contained in:
@@ -6,6 +6,8 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
@@ -14,6 +16,8 @@ import (
|
||||
|
||||
"git.ilapage.cn/OPC/chorus/internal/core/model"
|
||||
platformcrypto "git.ilapage.cn/OPC/chorus/internal/platform/crypto"
|
||||
"github.com/gin-gonic/gin"
|
||||
jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth"
|
||||
mysqldriver "github.com/go-sql-driver/mysql"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
@@ -183,6 +187,92 @@ func TestConnectivityChecksRequireAuthorizationAndReserveCooldownMySQL(t *testin
|
||||
}
|
||||
}
|
||||
|
||||
func TestChorusAPIMySQLRejectsUnauthorizedAndRedactsCredentials(t *testing.T) {
|
||||
db := openAdminTestDB(t)
|
||||
deleteAdminFixture(t, db, "api-")
|
||||
suffix := fmt.Sprintf("api-%d", time.Now().UnixNano())
|
||||
t.Cleanup(func() { deleteAdminFixture(t, db, suffix) })
|
||||
service := newAdminTestService(t, db, Config{})
|
||||
gin.SetMode(gin.TestMode)
|
||||
engine := gin.New()
|
||||
authentication := func(c *gin.Context) {
|
||||
if c.GetHeader("Authorization") != "Bearer test" {
|
||||
c.AbortWithStatus(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
c.Set(jwt.JwtPayloadKey, jwt.MapClaims{jwt.IdentityKey: float64(7)})
|
||||
c.Next()
|
||||
}
|
||||
authorization := func(c *gin.Context) {
|
||||
if c.GetHeader("X-Chorus-Role") != "operator" {
|
||||
c.AbortWithStatus(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
RegisterWithService(engine.Group("/api/v1"), authentication, authorization, service)
|
||||
|
||||
request := httptest.NewRequest(http.MethodGet, "/api/v1/chorus/providers", nil)
|
||||
response := httptest.NewRecorder()
|
||||
engine.ServeHTTP(response, request)
|
||||
if response.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("unauthorized status = %d, want %d", response.Code, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
secret := "synthetic-api-secret-" + suffix
|
||||
payload, err := json.Marshal(ProviderInput{
|
||||
Slug: "api-provider-" + suffix, Name: "API Provider " + suffix,
|
||||
BaseURL: "https://provider.invalid/v1", AuthType: "bearer", Enabled: true, APIKey: &secret,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
request = httptest.NewRequest(http.MethodPost, "/api/v1/chorus/providers", bytes.NewReader(payload))
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
request.Header.Set("Authorization", "Bearer test")
|
||||
request.Header.Set("X-Chorus-Role", "operator")
|
||||
request.Header.Set("X-Request-Id", "api-create-"+suffix)
|
||||
response = httptest.NewRecorder()
|
||||
engine.ServeHTTP(response, request)
|
||||
if response.Code != http.StatusOK {
|
||||
t.Fatalf("create provider status = %d, body = %s", response.Code, response.Body.String())
|
||||
}
|
||||
if strings.Contains(response.Body.String(), secret) || strings.Contains(response.Body.String(), "ciphertext") {
|
||||
t.Fatalf("create provider response contains protected credential data: %s", response.Body.String())
|
||||
}
|
||||
var envelope struct {
|
||||
Data ProviderView `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(response.Body.Bytes(), &envelope); err != nil {
|
||||
t.Fatalf("decode provider response: %v", err)
|
||||
}
|
||||
modelView, err := service.CreateProviderModel(context.Background(), 7, "api-model-"+suffix, ProviderModelInput{
|
||||
ProviderID: envelope.Data.ID, Name: "API Model " + suffix, ModelID: "api-model-" + suffix,
|
||||
APIType: model.APIChat, Kind: model.KindText, Capabilities: []model.Capability{model.CapabilityText},
|
||||
TimeoutMS: 1000, Weight: 100, Enabled: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create API connectivity fixture: %v", err)
|
||||
}
|
||||
request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/api/v1/chorus/provider-models/%d/connectivity-checks", modelView.ID), nil)
|
||||
request.Header.Set("Authorization", "Bearer test")
|
||||
request.Header.Set("X-Chorus-Role", "operator")
|
||||
request.Header.Set("X-Request-Id", "api-connectivity-"+suffix)
|
||||
response = httptest.NewRecorder()
|
||||
engine.ServeHTTP(response, request)
|
||||
if response.Code != http.StatusForbidden || !strings.Contains(response.Body.String(), "connectivity_not_authorized") {
|
||||
t.Fatalf("disabled connectivity response = %d %s", response.Code, response.Body.String())
|
||||
}
|
||||
request = httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/api/v1/chorus/providers/%d", envelope.Data.ID), nil)
|
||||
request.Header.Set("Authorization", "Bearer test")
|
||||
request.Header.Set("X-Chorus-Role", "operator")
|
||||
response = httptest.NewRecorder()
|
||||
engine.ServeHTTP(response, request)
|
||||
if response.Code != http.StatusNotFound {
|
||||
t.Fatalf("provider DELETE status = %d, want 404", response.Code)
|
||||
}
|
||||
}
|
||||
|
||||
type blockingProbe struct {
|
||||
calls atomic.Int32
|
||||
started chan struct{}
|
||||
|
||||
Reference in New Issue
Block a user