mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-23 22:35:03 +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:
@@ -119,7 +119,7 @@ func (s *ApiTokenService) Create(name, scope string, expiresAt int64) (*ApiToken
|
||||
|
||||
// RecreateByName replaces any token with this name, keeping exactly one so a
|
||||
// repeatedly-run caller cannot accumulate credentials it can never revoke.
|
||||
func (s *ApiTokenService) RecreateByName(name string) (*ApiTokenView, error) {
|
||||
func (s *ApiTokenService) RecreateByName(name, scope string) (*ApiTokenView, error) {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
return nil, common.NewError("token name is required")
|
||||
@@ -128,9 +128,39 @@ func (s *ApiTokenService) RecreateByName(name string) (*ApiTokenView, error) {
|
||||
if len(name) > 64 {
|
||||
return nil, common.NewError("token name must be 64 characters or fewer")
|
||||
}
|
||||
givenScope := ""
|
||||
if strings.TrimSpace(scope) != "" {
|
||||
var err error
|
||||
if givenScope, err = NormalizeScope(scope); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
plaintext := random.Seq(apiTokenLength)
|
||||
row := &model.ApiToken{Name: name, Token: crypto.HashTokenSHA256(plaintext), Enabled: true}
|
||||
row := &model.ApiToken{Name: name, Token: crypto.HashTokenSHA256(plaintext), Enabled: true, Scope: givenScope}
|
||||
if err := database.GetDB().Transaction(func(tx *gorm.DB) error {
|
||||
var replaced []model.ApiToken
|
||||
if err := tx.Where("name = ?", name).Order("id asc").Limit(1).Find(&replaced).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if len(replaced) > 0 {
|
||||
// A rotation keeps the deadline the token was issued with; reviving an
|
||||
// expired one would silently hand back a credential that never expires.
|
||||
if replaced[0].ExpiresAt != 0 && nowMilli() >= replaced[0].ExpiresAt {
|
||||
return common.NewErrorf("token %q has expired; create a new token from the panel or the API instead", name)
|
||||
}
|
||||
row.ExpiresAt = replaced[0].ExpiresAt
|
||||
}
|
||||
if row.Scope == "" {
|
||||
// An empty Scope takes the column default of admin, so a rotated
|
||||
// monitor or node-sync token would silently gain full access.
|
||||
row.Scope = model.ApiScopeAdmin
|
||||
if len(replaced) > 0 {
|
||||
if !model.IsKnownApiScope(replaced[0].Scope) {
|
||||
return common.NewErrorf("token %q has unknown scope %q", name, replaced[0].Scope)
|
||||
}
|
||||
row.Scope = replaced[0].Scope
|
||||
}
|
||||
}
|
||||
if err := tx.Where("name = ?", name).Delete(model.ApiToken{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user