139 lines
4.7 KiB
Go
139 lines
4.7 KiB
Go
package middleware
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"net/http"
|
||
|
|
"net/http/httptest"
|
||
|
|
"testing"
|
||
|
|
|
||
|
|
"github.com/volcano-tts/tts-api/setting"
|
||
|
|
)
|
||
|
|
|
||
|
|
// okHandler 是被保护的假 handler。
|
||
|
|
func okHandler() http.Handler {
|
||
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
w.WriteHeader(http.StatusOK)
|
||
|
|
_, _ = w.Write([]byte("ok"))
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestRequireAdmin_EmptyCredentialDenies 凭证未配置时必须拒绝,不能放行。
|
||
|
|
//
|
||
|
|
// 这是 v0.3.0 阶段 1 的核心修复:v0.3.0 之前 len(keys)==0 直接放行,
|
||
|
|
// 导致"没配 key"的部署上管理接口完全裸奔,也让后续给 /metrics、/health
|
||
|
|
// 套 RequireAdmin 的加固形同虚设。
|
||
|
|
func TestRequireAdmin_EmptyCredentialDenies(t *testing.T) {
|
||
|
|
t.Cleanup(func() { setting.SetAdminKeys(nil, "") })
|
||
|
|
setting.SetAdminKeys(nil, "")
|
||
|
|
|
||
|
|
h := RequireAdmin(okHandler())
|
||
|
|
|
||
|
|
// 完全不带 Authorization
|
||
|
|
w := httptest.NewRecorder()
|
||
|
|
h.ServeHTTP(w, httptest.NewRequest("GET", "/api/admin/overview", nil))
|
||
|
|
if w.Code != http.StatusUnauthorized {
|
||
|
|
t.Errorf("无凭证 + 未配置 admin credential: code=%d, want 401", w.Code)
|
||
|
|
}
|
||
|
|
|
||
|
|
// 带任意 Bearer token 也必须拒绝(列表为空,没有合法 token 可言)
|
||
|
|
w2 := httptest.NewRecorder()
|
||
|
|
r2 := httptest.NewRequest("GET", "/api/admin/overview", nil)
|
||
|
|
r2.Header.Set("Authorization", "Bearer anything")
|
||
|
|
h.ServeHTTP(w2, r2)
|
||
|
|
if w2.Code != http.StatusUnauthorized {
|
||
|
|
t.Errorf("任意 token + 未配置 admin credential: code=%d, want 401", w2.Code)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestRequireAdmin_ConfiguredCredentialEnforced 配了凭证后:命中放行、错误拒绝。
|
||
|
|
func TestRequireAdmin_ConfiguredCredentialEnforced(t *testing.T) {
|
||
|
|
t.Cleanup(func() { setting.SetAdminKeys(nil, "") })
|
||
|
|
setting.SetAdminKeys([]string{"admin-secret"}, "admin_key")
|
||
|
|
|
||
|
|
h := RequireAdmin(okHandler())
|
||
|
|
|
||
|
|
cases := []struct {
|
||
|
|
name string
|
||
|
|
authHeader string
|
||
|
|
wantStatus int
|
||
|
|
}{
|
||
|
|
{"正确凭证", "Bearer admin-secret", http.StatusOK},
|
||
|
|
{"错误凭证", "Bearer wrong-secret", http.StatusUnauthorized},
|
||
|
|
{"缺少 Bearer 前缀", "admin-secret", http.StatusUnauthorized},
|
||
|
|
{"空 Authorization", "", http.StatusUnauthorized},
|
||
|
|
{"仅空白 token", "Bearer ", http.StatusUnauthorized},
|
||
|
|
}
|
||
|
|
for _, c := range cases {
|
||
|
|
t.Run(c.name, func(t *testing.T) {
|
||
|
|
w := httptest.NewRecorder()
|
||
|
|
r := httptest.NewRequest("GET", "/api/admin/overview", nil)
|
||
|
|
if c.authHeader != "" {
|
||
|
|
r.Header.Set("Authorization", c.authHeader)
|
||
|
|
}
|
||
|
|
h.ServeHTTP(w, r)
|
||
|
|
if w.Code != c.wantStatus {
|
||
|
|
t.Errorf("code=%d, want %d", w.Code, c.wantStatus)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestRequireAdmin_OptionsAlwaysAllowed OPTIONS 预检必须放行(浏览器预检不带 Authorization)。
|
||
|
|
func TestRequireAdmin_OptionsAlwaysAllowed(t *testing.T) {
|
||
|
|
t.Cleanup(func() { setting.SetAdminKeys(nil, "") })
|
||
|
|
setting.SetAdminKeys([]string{"admin-secret"}, "admin_key")
|
||
|
|
|
||
|
|
w := httptest.NewRecorder()
|
||
|
|
h := RequireAdmin(okHandler())
|
||
|
|
h.ServeHTTP(w, httptest.NewRequest("OPTIONS", "/api/admin/overview", nil))
|
||
|
|
if w.Code != http.StatusOK {
|
||
|
|
t.Errorf("OPTIONS: code=%d, want 200", w.Code)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestAdminAndBusinessCredentialIsolation 权限隔离:
|
||
|
|
// 配置了独立 admin_key 后,业务凭证(auth_key)不得访问管理接口;
|
||
|
|
// 未配置 admin_key 时回退,业务凭证仍可管理(向后兼容)。
|
||
|
|
func TestAdminAndBusinessCredentialIsolation(t *testing.T) {
|
||
|
|
t.Cleanup(func() {
|
||
|
|
setting.SetAdminKeys(nil, "")
|
||
|
|
setting.SetAuthAPIKeys(nil)
|
||
|
|
})
|
||
|
|
|
||
|
|
const businessKey = "business-key"
|
||
|
|
const adminKey = "admin-key"
|
||
|
|
|
||
|
|
// 业务侧凭证固定为 businessKey(模拟 /v1/audio/speech 用的 key)
|
||
|
|
setting.SetAuthAPIKeys([]string{businessKey})
|
||
|
|
|
||
|
|
// --- 场景 A:配置了独立 admin_key(隔离生效) ---
|
||
|
|
setting.SetAdminKeys([]string{adminKey}, "admin_key")
|
||
|
|
h := RequireAdmin(okHandler())
|
||
|
|
|
||
|
|
wBiz := httptest.NewRecorder()
|
||
|
|
rBiz := httptest.NewRequest("GET", "/api/admin/overview", nil)
|
||
|
|
rBiz.Header.Set("Authorization", "Bearer "+businessKey)
|
||
|
|
h.ServeHTTP(wBiz, rBiz)
|
||
|
|
if wBiz.Code != http.StatusUnauthorized {
|
||
|
|
t.Errorf("隔离生效时,业务 key 访问管理接口: code=%d, want 401", wBiz.Code)
|
||
|
|
}
|
||
|
|
|
||
|
|
wAdmin := httptest.NewRecorder()
|
||
|
|
rAdmin := httptest.NewRequest("GET", "/api/admin/overview", nil)
|
||
|
|
rAdmin.Header.Set("Authorization", "Bearer "+adminKey)
|
||
|
|
h.ServeHTTP(wAdmin, rAdmin)
|
||
|
|
if wAdmin.Code != http.StatusOK {
|
||
|
|
t.Errorf("隔离生效时,admin_key 访问管理接口: code=%d, want 200", wAdmin.Code)
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- 场景 B:未配置 admin_key,回退用业务凭证(向后兼容) ---
|
||
|
|
setting.SetAdminKeys([]string{businessKey}, "auth_key")
|
||
|
|
|
||
|
|
wFallback := httptest.NewRecorder()
|
||
|
|
rFallback := httptest.NewRequest("GET", "/api/admin/overview", nil)
|
||
|
|
rFallback.Header.Set("Authorization", "Bearer "+businessKey)
|
||
|
|
h.ServeHTTP(wFallback, rFallback)
|
||
|
|
if wFallback.Code != http.StatusOK {
|
||
|
|
t.Errorf("回退模式下,业务 key 应可管理: code=%d, want 200", wFallback.Code)
|
||
|
|
}
|
||
|
|
}
|