Some checks are pending
Security Checks / security (push) Waiting to run
统一 frontend、backend、test、test-log 和 out 目录,更新构建、部署及测试入口;纳入配置管理兼容与首次安全引导修复;排除本机配置、密钥和测试产物。
152 lines
5.0 KiB
Go
152 lines
5.0 KiB
Go
package controllers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.pcux.cn/xuans/Smail/config"
|
|
"git.pcux.cn/xuans/Smail/db"
|
|
"git.pcux.cn/xuans/Smail/models"
|
|
"git.pcux.cn/xuans/Smail/services/auth"
|
|
appcontext "git.pcux.cn/xuans/Smail/utils/context"
|
|
"git.pcux.cn/xuans/Smail/utils/password"
|
|
)
|
|
|
|
func TestEditUserProtectsAdministrativeCredentialsAndRevokesUserSessions(t *testing.T) {
|
|
oldConfig := config.Instance
|
|
oldDB := db.Instance
|
|
testRoot := t.TempDir()
|
|
t.Cleanup(func() {
|
|
if db.Instance != nil && db.Instance != oldDB {
|
|
_ = db.Instance.Close()
|
|
}
|
|
config.Instance = oldConfig
|
|
db.Instance = oldDB
|
|
})
|
|
|
|
config.Instance = &config.Config{DbType: config.DBTypeSQLite, DbDSN: filepath.Join(testRoot, "user-security.db")}
|
|
if err := db.Init(""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
admin := models.User{Account: "admin-user", Name: "Admin", IsAdmin: 1, SessionVersion: 1}
|
|
if err := auth.SetPassword(&admin, "AdminPass123"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
regular := models.User{Account: "regular-user", Name: "User", SessionVersion: 1}
|
|
if err := auth.SetPassword(®ular, "UserPass123"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := db.Instance.Insert(&admin, ®ular); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ctx := &appcontext.Context{
|
|
UserID: admin.ID, UserAccount: admin.Account, IsAdmin: true, Lang: "zhCn",
|
|
RecentAuth: true, RecoveryCodeConfigured: true,
|
|
}
|
|
|
|
result := editUserRequest(t, ctx, `{"id":`+itoa(admin.ID)+`,"password":"BypassPass123","disabled":0}`)
|
|
if result == 0 {
|
|
t.Fatal("administrator changed the current password without current-password verification")
|
|
}
|
|
var loadedAdmin models.User
|
|
if _, err := db.Instance.ID(admin.ID).Get(&loadedAdmin); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !password.Verify(loadedAdmin.Password, "AdminPass123") {
|
|
t.Fatal("administrator password changed through the user management bypass")
|
|
}
|
|
|
|
result = editUserRequest(t, ctx, `{"id":`+itoa(admin.ID)+`,"disabled":1}`)
|
|
if result == 0 {
|
|
t.Fatal("administrator account was disabled")
|
|
}
|
|
|
|
result = editUserRequest(t, ctx, `{"id":`+itoa(regular.ID)+`,"password":"ResetPass123","disabled":0}`)
|
|
if result != 0 {
|
|
t.Fatal("administrator could not reset another user password")
|
|
}
|
|
var resetUser models.User
|
|
if _, err := db.Instance.ID(regular.ID).Get(&resetUser); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if resetUser.SessionVersion != regular.SessionVersion+1 || !password.Verify(resetUser.Password, "ResetPass123") {
|
|
t.Fatalf("password reset did not revoke existing sessions: version=%d", resetUser.SessionVersion)
|
|
}
|
|
|
|
result = editUserRequest(t, ctx, `{"id":`+itoa(regular.ID)+`,"disabled":1}`)
|
|
if result != 0 {
|
|
t.Fatal("administrator could not disable a regular user")
|
|
}
|
|
var disabledUser models.User
|
|
if _, err := db.Instance.ID(regular.ID).Get(&disabledUser); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if disabledUser.Disabled != 1 || disabledUser.SessionVersion != resetUser.SessionVersion+1 {
|
|
t.Fatalf("disabling a user did not revoke existing sessions: %+v", disabledUser)
|
|
}
|
|
}
|
|
|
|
func editUserRequest(t *testing.T, ctx *appcontext.Context, payload string) int {
|
|
t.Helper()
|
|
recorder := httptest.NewRecorder()
|
|
request := httptest.NewRequest(http.MethodPost, "/api/user/edit", strings.NewReader(payload))
|
|
EditUser(ctx, recorder, request)
|
|
var result struct {
|
|
ErrorNo int `json:"errorNo"`
|
|
}
|
|
if err := json.Unmarshal(recorder.Body.Bytes(), &result); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return result.ErrorNo
|
|
}
|
|
|
|
func itoa(value int) string {
|
|
if value == 0 {
|
|
return "0"
|
|
}
|
|
digits := make([]byte, 0, 20)
|
|
for value > 0 {
|
|
digits = append(digits, byte('0'+value%10))
|
|
value /= 10
|
|
}
|
|
for left, right := 0, len(digits)-1; left < right; left, right = left+1, right-1 {
|
|
digits[left], digits[right] = digits[right], digits[left]
|
|
}
|
|
return string(digits)
|
|
}
|
|
|
|
func TestUserRequestsRejectInvalidJSON(t *testing.T) {
|
|
ctx := &appcontext.Context{IsAdmin: true, Lang: "zhCn"}
|
|
tests := []struct {
|
|
name string
|
|
handler func(*appcontext.Context, http.ResponseWriter, *http.Request)
|
|
payload string
|
|
}{
|
|
{name: "list malformed", handler: UserList, payload: `{"current_page":`},
|
|
{name: "list trailing", handler: UserList, payload: `{"current_page":1,"page_size":15} {}`},
|
|
{name: "list oversized", handler: UserList, payload: `{"current_page":1,"page_size":15,"padding":"` + strings.Repeat("a", int(maxUserRequestBytes)) + `"}`},
|
|
{name: "create trailing", handler: CreateUser, payload: `{"account":"user","username":"User","password":"Password123"} {}`},
|
|
{name: "edit trailing", handler: EditUser, payload: `{"id":1,"disabled":0} {}`},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
recorder := httptest.NewRecorder()
|
|
request := httptest.NewRequest(http.MethodPost, "/api/user", strings.NewReader(test.payload))
|
|
test.handler(ctx, recorder, request)
|
|
var result struct {
|
|
ErrorNo int `json:"errorNo"`
|
|
}
|
|
if err := json.Unmarshal(recorder.Body.Bytes(), &result); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if result.ErrorNo == 0 {
|
|
t.Fatalf("invalid request was accepted: %s", test.name)
|
|
}
|
|
})
|
|
}
|
|
}
|