mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-19 12:25:00 +03:00
fix(auth): add node-admin token scope for panel updates (#6666)
* 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>
This commit is contained in:
@@ -107,6 +107,43 @@ func TestCheckAPIAuth_BearerSuccess(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPanelUpdateRequiresNodeAdminScope(t *testing.T) {
|
||||
engine, _ := newAPIAuthTestEngine(t)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
scope string
|
||||
wantCode int
|
||||
wantBody string
|
||||
}{
|
||||
{"node sync denied", model.ApiScopeNodeSync, http.StatusForbidden, `{"msg":"this API token is not permitted to access this endpoint","success":false}`},
|
||||
{"node admin allowed", model.ApiScopeNodeAdmin, http.StatusOK, `{"reached":true}`},
|
||||
{"monitor denied", model.ApiScopeMonitor, http.StatusForbidden, `{"msg":"this API token is not permitted to access this endpoint","success":false}`},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
plaintext := tc.name + "-token"
|
||||
if err := database.GetDB().Create(&model.ApiToken{
|
||||
Name: tc.name,
|
||||
Token: crypto.HashTokenSHA256(plaintext),
|
||||
Enabled: true,
|
||||
Scope: tc.scope,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("seed token: %v", err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/panel/api/server/updatePanel", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+plaintext)
|
||||
w := httptest.NewRecorder()
|
||||
engine.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != tc.wantCode || w.Body.String() != tc.wantBody {
|
||||
t.Fatalf("response = %d %s, want %d %s", w.Code, w.Body.String(), tc.wantCode, tc.wantBody)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestCheckAPIAuth_AcceptsVerifiedClientCert ensures verified mTLS authenticates
|
||||
// as node-sync rather than bypassing scope checks as admin.
|
||||
func TestCheckAPIAuth_AcceptsVerifiedClientCert(t *testing.T) {
|
||||
@@ -135,6 +172,48 @@ func TestCheckAPIAuth_AcceptsVerifiedClientCert(t *testing.T) {
|
||||
if w.Code != http.StatusForbidden {
|
||||
t.Fatalf("updatePanel status = %d, want 403; body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
for _, legacyScope := range []string{model.ApiScopeAdmin, model.ApiScopeMonitor, model.ApiScopeNodeSync} {
|
||||
plaintext := "mtls-" + legacyScope + "-token"
|
||||
if err := database.GetDB().Create(&model.ApiToken{
|
||||
Name: "mtls-" + legacyScope,
|
||||
Token: crypto.HashTokenSHA256(plaintext),
|
||||
Enabled: true,
|
||||
Scope: legacyScope,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("seed %s token: %v", legacyScope, err)
|
||||
}
|
||||
legacy := httptest.NewRequest(http.MethodGet, "/panel/api/server/status", nil)
|
||||
legacy.Header.Set("Authorization", "Bearer "+plaintext)
|
||||
legacy.TLS = &tls.ConnectionState{
|
||||
VerifiedChains: [][]*x509.Certificate{{&x509.Certificate{}}},
|
||||
}
|
||||
w = httptest.NewRecorder()
|
||||
engine.ServeHTTP(w, legacy)
|
||||
if w.Code != http.StatusOK || w.Body.String() != `{"api_authed":true,"scope":"node-sync"}` {
|
||||
t.Fatalf("mTLS %s response = %d %s, want preserved node-sync scope", legacyScope, w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
const plaintext = "mtls-node-admin-token"
|
||||
if err := database.GetDB().Create(&model.ApiToken{
|
||||
Name: "mtls-node-admin",
|
||||
Token: crypto.HashTokenSHA256(plaintext),
|
||||
Enabled: true,
|
||||
Scope: model.ApiScopeNodeAdmin,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("seed node-admin token: %v", err)
|
||||
}
|
||||
allowed := httptest.NewRequest(http.MethodPost, "/panel/api/server/updatePanel", nil)
|
||||
allowed.Header.Set("Authorization", "Bearer "+plaintext)
|
||||
allowed.TLS = &tls.ConnectionState{
|
||||
VerifiedChains: [][]*x509.Certificate{{&x509.Certificate{}}},
|
||||
}
|
||||
w = httptest.NewRecorder()
|
||||
engine.ServeHTTP(w, allowed)
|
||||
if w.Code != http.StatusOK || w.Body.String() != `{"reached":true}` {
|
||||
t.Fatalf("mTLS node-admin response = %d %s, want 200 update launch", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeSyncScopeUsesFullPathPatterns(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user