- internal/auth/mode.go:Mode 解析/归一化,X-Auth-User 头常量 - server:adminAuth 按模式鉴权,sso 只认网关注入的 X-Auth-User(缺失 401 JSON) - 新增公开 /api/admin/mode 供前端识别模式;sso 下 login/logout 返回 404 - main:读取 AUTH_MODE,sso 时跳过自带口令并打印启动提示 - 测试覆盖两种模式、伪造 header 不绕过、数据面不受影响
176 lines
6.0 KiB
Go
176 lines
6.0 KiB
Go
package server
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/YLing2024/davbox/internal/account"
|
|
"github.com/YLing2024/davbox/internal/auth"
|
|
)
|
|
|
|
// newSSOTestServer 构造一个 SSO 模式的测试服务器。
|
|
func newSSOTestServer(t *testing.T) (*Server, *account.Store, string) {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
store, err := account.Open(dir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
signer := auth.NewSigner([]byte("0123456789abcdef0123456789abcdef"))
|
|
return New(Config{
|
|
DataDir: dir,
|
|
Store: store,
|
|
Signer: signer,
|
|
AuthMode: auth.ModeSSO,
|
|
}), store, dir
|
|
}
|
|
|
|
func TestAdminModeEndpoint(t *testing.T) {
|
|
// builtin 默认
|
|
s, _, _ := newTestServer(t)
|
|
rec := request(s, http.MethodGet, "/api/admin/mode", "", "", nil, nil)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("mode 应为 200,实际 %d", rec.Code)
|
|
}
|
|
var got struct {
|
|
Mode string `json:"mode"`
|
|
}
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.Mode != "builtin" {
|
|
t.Fatalf("默认模式应为 builtin,实际 %q", got.Mode)
|
|
}
|
|
|
|
// sso
|
|
s2, _, _ := newSSOTestServer(t)
|
|
rec = request(s2, http.MethodGet, "/api/admin/mode", "", "", nil, nil)
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.Mode != "sso" {
|
|
t.Fatalf("sso 模式应为 sso,实际 %q", got.Mode)
|
|
}
|
|
}
|
|
|
|
func TestAdminSSOHeaderAccepted(t *testing.T) {
|
|
s, _, _ := newSSOTestServer(t)
|
|
rec := request(s, http.MethodGet, "/api/admin/accounts", "", "", nil, map[string]string{"X-Auth-User": "linden"})
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("带 X-Auth-User 应为 200,实际 %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestAdminSSOMissingHeaderIs401JSON(t *testing.T) {
|
|
s, _, _ := newSSOTestServer(t)
|
|
rec := request(s, http.MethodGet, "/api/admin/accounts", "", "", nil, nil)
|
|
if rec.Code != http.StatusUnauthorized {
|
|
t.Fatalf("缺 header 应为 401,实际 %d", rec.Code)
|
|
}
|
|
if ct := rec.Header().Get("Content-Type"); !strings.HasPrefix(ct, "application/json") {
|
|
t.Fatalf("401 应为 JSON,实际 Content-Type=%q", ct)
|
|
}
|
|
if loc := rec.Header().Get("Location"); loc != "" {
|
|
t.Fatalf("401 不应重定向,实际 Location=%q", loc)
|
|
}
|
|
var body map[string]string
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
|
t.Fatalf("响应体不是 JSON: %v (%q)", err, rec.Body.String())
|
|
}
|
|
if body["error"] == "" {
|
|
t.Fatalf("JSON 应含 error 字段,实际 %q", rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestAdminSSOEmptyHeaderIs401(t *testing.T) {
|
|
s, _, _ := newSSOTestServer(t)
|
|
rec := request(s, http.MethodGet, "/api/admin/accounts", "", "", nil, map[string]string{"X-Auth-User": " "})
|
|
if rec.Code != http.StatusUnauthorized {
|
|
t.Fatalf("空白 X-Auth-User 应为 401,实际 %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestAdminBuiltinIgnoresAuthUserHeader(t *testing.T) {
|
|
// builtin 模式下伪造 X-Auth-User 不能绕过口令认证。
|
|
s, _, _ := newTestServer(t)
|
|
rec := request(s, http.MethodGet, "/api/admin/accounts", "", "", nil, map[string]string{"X-Auth-User": "linden"})
|
|
if rec.Code != http.StatusUnauthorized {
|
|
t.Fatalf("builtin 模式下伪造 header 应为 401,实际 %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestAdminSSOLoginLogoutNotFound(t *testing.T) {
|
|
s, _, _ := newSSOTestServer(t)
|
|
rec := request(s, http.MethodPost, "/api/admin/login", "", "", strings.NewReader(`{"password":"x"}`), nil)
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Fatalf("sso 模式 login 应为 404,实际 %d", rec.Code)
|
|
}
|
|
rec = request(s, http.MethodPost, "/api/admin/logout", "", "", nil, map[string]string{"X-Auth-User": "linden"})
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Fatalf("sso 模式 logout 应为 404,实际 %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestAdminSSOAccountCRUD(t *testing.T) {
|
|
s, _, _ := newSSOTestServer(t)
|
|
h := map[string]string{"X-Auth-User": "linden"}
|
|
|
|
rec := request(s, http.MethodPost, "/api/admin/accounts", "", "", strings.NewReader(`{"user":"ssoapp","readonly":false}`), h)
|
|
if rec.Code != http.StatusCreated {
|
|
t.Fatalf("sso 建账号应为 201,实际 %d", rec.Code)
|
|
}
|
|
rec = request(s, http.MethodGet, "/api/admin/accounts", "", "", nil, h)
|
|
if rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), "ssoapp") {
|
|
t.Fatalf("sso 列表应含新账号,实际 %d %q", rec.Code, rec.Body.String())
|
|
}
|
|
rec = request(s, http.MethodDelete, "/api/admin/accounts/ssoapp", "", "", nil, h)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("sso 删除应为 200,实际 %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
// SSO 模式不得影响 WebDAV 数据面与 client 接口。
|
|
func TestSSOModeDoesNotAffectDataPlane(t *testing.T) {
|
|
s, store, _ := newSSOTestServer(t)
|
|
_, pass, _ := store.Create("app1", false, "")
|
|
|
|
rec := request(s, "PROPFIND", "/app1/", "app1", pass, nil, map[string]string{"Depth": "0"})
|
|
if rec.Code != 207 {
|
|
t.Fatalf("SSO 下 WebDAV 认证后应为 207,实际 %d", rec.Code)
|
|
}
|
|
rec = request(s, "PROPFIND", "/app1/", "", "", nil, map[string]string{"Depth": "0"})
|
|
if rec.Code != http.StatusUnauthorized {
|
|
t.Fatalf("SSO 下 WebDAV 匿名应为 401,实际 %d", rec.Code)
|
|
}
|
|
|
|
rec = request(s, http.MethodPost, "/api/client/login", "", "", strings.NewReader(`{"user":"app1","pass":"`+pass+`"}`), nil)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("SSO 下 client 登录应为 200,实际 %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
// 静态资源在 SSO 模式下仍可访问,不受管理端鉴权影响。
|
|
func TestSSOModeStaticUnaffected(t *testing.T) {
|
|
s, _, _ := newSSOTestServer(t)
|
|
rec := request(s, http.MethodGet, "/admin", "", "", nil, nil)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("/admin 页面应为 200,实际 %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
// 保证 builtin 回归:现有显式流程仍可用(含 cookie 登录)。
|
|
func TestBuiltinModeStillWorks(t *testing.T) {
|
|
s, _, _ := newTestServer(t)
|
|
rec := request(s, http.MethodPost, "/api/admin/login", "", "", strings.NewReader(`{"password":"`+testAdminPass+`"}`), nil)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("builtin 登录应为 200,实际 %d", rec.Code)
|
|
}
|
|
cookies := rec.Result().Cookies()
|
|
rec = doWithCookie(s, http.MethodGet, "/api/admin/accounts", cookies, nil)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("builtin 带 cookie 列表应为 200,实际 %d", rec.Code)
|
|
}
|
|
}
|