mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-05 05:32:07 +03:00
fix(api-token): keep a token's scope when -getApiToken regenerates it, add -tokenScope (#6700)
* fix(api-token): keep a token's scope when the CLI regenerates it RecreateByName deleted the named row and created a new one without a Scope, so the insert took the column default of admin. Since -tokenName lets the CLI regenerate any token, rotating a monitor or node-sync token silently turned it into a full-access one. The replacement now takes the scope of the row it replaces, and a new name still gets admin as before. A stored scope this build does not know, as after a downgrade, fails the rotation and leaves the row alone instead of guessing. Assisted-by: Claude Code:claude-opus-5-5 (mostly) * feat(cli): let -getApiToken choose the scope of the token it issues -tokenScope sets the scope on both branches of -getApiToken: the token minted on a fresh panel and the one regenerated on a populated panel. Without the flag a regenerated token keeps its scope and a new one gets admin, so every existing invocation, install.sh included, behaves as before. An unknown scope is refused before anything is deleted, so a typo cannot revoke the token it meant to rotate. Assisted-by: Claude Code:claude-opus-5-5 (mostly) * fix(api-token): keep a token's expiry when the CLI regenerates it RecreateByName built the replacement row with ExpiresAt 0, so running `x-ui setting -getApiToken -tokenName <name>` on a token issued through the API with a deadline handed back one that never expires, and said nothing about it - the same silent widening this branch fixed for scope. The replacement now carries the replaced row's ExpiresAt. A token whose deadline has already passed is refused instead of rotated, since keeping the deadline would mint a dead token and dropping it would revive an expired credential without limit; the expired row is left untouched. --------- Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
This commit is contained in:
+23
-7
@@ -57,12 +57,12 @@ func TestGetApiTokenRotatesOnlyTheNamedToken(t *testing.T) {
|
||||
newTokenCLIEnv(t)
|
||||
|
||||
svc := panel.ApiTokenService{}
|
||||
weekly, err := svc.RecreateByName("weekly-report")
|
||||
weekly, err := svc.RecreateByName("weekly-report", "")
|
||||
if err != nil {
|
||||
t.Fatalf("seed weekly-report: %v", err)
|
||||
}
|
||||
|
||||
GetApiToken(true, "ci-bot")
|
||||
GetApiToken(true, "ci-bot", "")
|
||||
|
||||
names := tokenNames(t)
|
||||
if !hasName(names, "ci-bot") {
|
||||
@@ -78,7 +78,7 @@ func TestGetApiTokenRotatesOnlyTheNamedToken(t *testing.T) {
|
||||
func TestGetApiTokenUsesGivenNameOnEmptyDatabase(t *testing.T) {
|
||||
newTokenCLIEnv(t)
|
||||
|
||||
GetApiToken(true, "ci-bot")
|
||||
GetApiToken(true, "ci-bot", "")
|
||||
|
||||
names := tokenNames(t)
|
||||
if !hasName(names, "ci-bot") {
|
||||
@@ -89,15 +89,31 @@ func TestGetApiTokenUsesGivenNameOnEmptyDatabase(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// -tokenScope has to reach both branches, or a fresh panel would mint an admin
|
||||
// token for a caller that asked for monitor.
|
||||
func TestGetApiTokenAppliesGivenScope(t *testing.T) {
|
||||
newTokenCLIEnv(t)
|
||||
|
||||
GetApiToken(true, "ci-bot", model.ApiScopeMonitor)
|
||||
if got := tokenRow(t, "ci-bot").Scope; got != model.ApiScopeMonitor {
|
||||
t.Fatalf("minted scope = %q, want %q", got, model.ApiScopeMonitor)
|
||||
}
|
||||
|
||||
GetApiToken(true, "ci-bot", model.ApiScopeNodeSync)
|
||||
if got := tokenRow(t, "ci-bot").Scope; got != model.ApiScopeNodeSync {
|
||||
t.Fatalf("regenerated scope = %q, want %q", got, model.ApiScopeNodeSync)
|
||||
}
|
||||
}
|
||||
|
||||
// install.sh records the token it gets on a fresh panel. A later bare
|
||||
// -getApiToken must rotate the fallback slot and leave that record valid.
|
||||
func TestGetApiTokenPreservesInstallTokenWhenRotating(t *testing.T) {
|
||||
newTokenCLIEnv(t)
|
||||
|
||||
GetApiToken(true, "")
|
||||
GetApiToken(true, "", "")
|
||||
installed := tokenRow(t, installTokenName)
|
||||
|
||||
GetApiToken(true, "")
|
||||
GetApiToken(true, "", "")
|
||||
|
||||
names := tokenNames(t)
|
||||
if !hasName(names, cliFallbackTokenName) {
|
||||
@@ -134,10 +150,10 @@ func TestGetApiTokenWarnsOnIgnoredPositionalArgs(t *testing.T) {
|
||||
func TestGetApiTokenTrimsName(t *testing.T) {
|
||||
newTokenCLIEnv(t)
|
||||
|
||||
if _, err := (&panel.ApiTokenService{}).RecreateByName("seed"); err != nil {
|
||||
if _, err := (&panel.ApiTokenService{}).RecreateByName("seed", ""); err != nil {
|
||||
t.Fatalf("seed: %v", err)
|
||||
}
|
||||
GetApiToken(true, " ")
|
||||
GetApiToken(true, " ", "")
|
||||
|
||||
names := tokenNames(t)
|
||||
if !hasName(names, cliFallbackTokenName) {
|
||||
|
||||
Reference in New Issue
Block a user