mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-18 03:45:02 +03:00
a8d0700bf2
* feat(auth): add node-admin API token scope Keep ordinary node-sync credentials unable to self-update a remote panel while allowing explicitly elevated node administrators to inherit synchronization access and invoke updatePanel. * style(docs): format API scope table * fix(auth): preserve mTLS node-sync semantics Allow only an explicit node-admin bearer to elevate a verified mTLS request. Existing admin, monitor, and node-sync bearer values continue to use the historical node-sync identity on mTLS connections. * chore(ci): rerun flaky checks --------- Co-authored-by: libmur-dev <333915961+libmur-dev@users.noreply.github.com>
106 lines
3.5 KiB
Go
106 lines
3.5 KiB
Go
package controller
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/dbtest"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/util/crypto"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/web/runtime"
|
|
)
|
|
|
|
// A sub-node stores whatever the master pushes. A master row whose certificate
|
|
// predates the TLS guard must still land, or the node silently falls out of sync.
|
|
func TestNodeSyncPushSkipsOperatorTLSGuard(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
dbDir := t.TempDir()
|
|
t.Setenv("XUI_DB_FOLDER", dbDir)
|
|
dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db"))
|
|
prev := runtime.GetManager()
|
|
runtime.SetManager(runtime.NewManager(runtime.LocalDeps{APIPort: func() int { return 0 }, SetNeedRestart: func() {}}))
|
|
t.Cleanup(func() { runtime.SetManager(prev) })
|
|
|
|
for name, scope := range map[string]string{
|
|
"node-sync": model.ApiScopeNodeSync,
|
|
"node-admin": model.ApiScopeNodeAdmin,
|
|
"admin": model.ApiScopeAdmin,
|
|
} {
|
|
row := &model.ApiToken{Name: name, Token: crypto.HashTokenSHA256(name + "-token"), Enabled: true, Scope: scope}
|
|
if err := database.GetDB().Create(row).Error; err != nil {
|
|
t.Fatalf("seed %s token: %v", name, err)
|
|
}
|
|
}
|
|
|
|
engine := gin.New()
|
|
a := &APIController{}
|
|
api := engine.Group("/panel/api")
|
|
api.Use(a.checkAPIAuth, a.enforceTokenScope)
|
|
NewInboundController(api.Group("/inbounds"))
|
|
|
|
const legacyStream = `{"network":"tcp","security":"tls","tlsSettings":{"certificates":[{"certificateFile":"","keyFile":"","certificate":[],"key":[]}]}}`
|
|
add := func(t *testing.T, token string, port int) string {
|
|
t.Helper()
|
|
form := url.Values{
|
|
"protocol": {"vless"},
|
|
"port": {strconv.Itoa(port)},
|
|
"tag": {"tls-legacy-" + strconv.Itoa(port)},
|
|
"enable": {"true"},
|
|
"settings": {`{"clients":[]}`},
|
|
"streamSettings": {legacyStream},
|
|
}
|
|
req := httptest.NewRequest(http.MethodPost, "/panel/api/inbounds/add", strings.NewReader(form.Encode()))
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
w := httptest.NewRecorder()
|
|
engine.ServeHTTP(w, req)
|
|
return w.Body.String()
|
|
}
|
|
rows := func(t *testing.T, tag string) int64 {
|
|
t.Helper()
|
|
var n int64
|
|
if err := database.GetDB().Model(&model.Inbound{}).Where("tag = ?", tag).Count(&n).Error; err != nil {
|
|
t.Fatalf("count %s: %v", tag, err)
|
|
}
|
|
return n
|
|
}
|
|
|
|
t.Run("a master push lands on the node", func(t *testing.T) {
|
|
body := add(t, "node-sync-token", 45001)
|
|
if !strings.Contains(body, `"success":true`) {
|
|
t.Fatalf("node-sync add rejected: %s", body)
|
|
}
|
|
if got := rows(t, "tls-legacy-45001"); got != 1 {
|
|
t.Fatalf("stored rows = %d, want 1", got)
|
|
}
|
|
})
|
|
|
|
t.Run("a node admin push keeps node sync semantics", func(t *testing.T) {
|
|
body := add(t, "node-admin-token", 45003)
|
|
if !strings.Contains(body, `"success":true`) {
|
|
t.Fatalf("node-admin add rejected: %s", body)
|
|
}
|
|
if got := rows(t, "tls-legacy-45003"); got != 1 {
|
|
t.Fatalf("stored rows = %d, want 1", got)
|
|
}
|
|
})
|
|
|
|
t.Run("an operator token is still held to the guard", func(t *testing.T) {
|
|
body := add(t, "admin-token", 45002)
|
|
if !strings.Contains(body, `"success":false`) || !strings.Contains(body, "TLS") {
|
|
t.Fatalf("admin add should fail on TLS, got: %s", body)
|
|
}
|
|
if got := rows(t, "tls-legacy-45002"); got != 0 {
|
|
t.Fatalf("stored rows = %d, want 0", got)
|
|
}
|
|
})
|
|
}
|