1
0

api_token_cli_test.go 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. package main
  2. // GetApiToken rotates a credential rather than displaying one, so these pin
  3. // which token name it destroys — the whole point of the -tokenName flag.
  4. import (
  5. "flag"
  6. "testing"
  7. "github.com/mhsanaei/3x-ui/v3/internal/config"
  8. "github.com/mhsanaei/3x-ui/v3/internal/database"
  9. "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest"
  10. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  11. "github.com/mhsanaei/3x-ui/v3/internal/web/service/panel"
  12. )
  13. func newTokenCLIEnv(t *testing.T) {
  14. t.Helper()
  15. t.Setenv("XUI_DB_FOLDER", t.TempDir())
  16. dbtest.InitDB(t, config.GetDBPath())
  17. }
  18. func tokenNames(t *testing.T) []string {
  19. t.Helper()
  20. tokens, err := (&panel.ApiTokenService{}).List()
  21. if err != nil {
  22. t.Fatalf("list tokens: %v", err)
  23. }
  24. names := make([]string, 0, len(tokens))
  25. for _, token := range tokens {
  26. names = append(names, token.Name)
  27. }
  28. return names
  29. }
  30. func tokenRow(t *testing.T, name string) model.ApiToken {
  31. t.Helper()
  32. var row model.ApiToken
  33. if err := database.GetDB().Where("name = ?", name).First(&row).Error; err != nil {
  34. t.Fatalf("load token %q: %v", name, err)
  35. }
  36. return row
  37. }
  38. func hasName(names []string, want string) bool {
  39. for _, name := range names {
  40. if name == want {
  41. return true
  42. }
  43. }
  44. return false
  45. }
  46. // The bug: two callers sharing one hardcoded slot silently revoke each other.
  47. // A named token must leave an differently-named one authenticating.
  48. func TestGetApiTokenRotatesOnlyTheNamedToken(t *testing.T) {
  49. newTokenCLIEnv(t)
  50. svc := panel.ApiTokenService{}
  51. weekly, err := svc.RecreateByName("weekly-report", "")
  52. if err != nil {
  53. t.Fatalf("seed weekly-report: %v", err)
  54. }
  55. GetApiToken(true, "ci-bot", "")
  56. names := tokenNames(t)
  57. if !hasName(names, "ci-bot") {
  58. t.Fatalf("token names = %v, want ci-bot among them", names)
  59. }
  60. if !svc.Match(weekly.Token) {
  61. t.Fatal("weekly-report was revoked by a call naming ci-bot")
  62. }
  63. }
  64. // An explicit name has to win on both branches, or the same command would
  65. // produce ci-bot on a populated panel and "install" on a fresh one.
  66. func TestGetApiTokenUsesGivenNameOnEmptyDatabase(t *testing.T) {
  67. newTokenCLIEnv(t)
  68. GetApiToken(true, "ci-bot", "")
  69. names := tokenNames(t)
  70. if !hasName(names, "ci-bot") {
  71. t.Fatalf("token names = %v, want ci-bot among them", names)
  72. }
  73. if hasName(names, installTokenName) {
  74. t.Fatalf("token names = %v, want no %s when a name was given", names, installTokenName)
  75. }
  76. }
  77. // -tokenScope has to reach both branches, or a fresh panel would mint an admin
  78. // token for a caller that asked for monitor.
  79. func TestGetApiTokenAppliesGivenScope(t *testing.T) {
  80. newTokenCLIEnv(t)
  81. GetApiToken(true, "ci-bot", model.ApiScopeMonitor)
  82. if got := tokenRow(t, "ci-bot").Scope; got != model.ApiScopeMonitor {
  83. t.Fatalf("minted scope = %q, want %q", got, model.ApiScopeMonitor)
  84. }
  85. GetApiToken(true, "ci-bot", model.ApiScopeNodeSync)
  86. if got := tokenRow(t, "ci-bot").Scope; got != model.ApiScopeNodeSync {
  87. t.Fatalf("regenerated scope = %q, want %q", got, model.ApiScopeNodeSync)
  88. }
  89. }
  90. // install.sh records the token it gets on a fresh panel. A later bare
  91. // -getApiToken must rotate the fallback slot and leave that record valid.
  92. func TestGetApiTokenPreservesInstallTokenWhenRotating(t *testing.T) {
  93. newTokenCLIEnv(t)
  94. GetApiToken(true, "", "")
  95. installed := tokenRow(t, installTokenName)
  96. GetApiToken(true, "", "")
  97. names := tokenNames(t)
  98. if !hasName(names, cliFallbackTokenName) {
  99. t.Fatalf("token names = %v, want %s among them", names, cliFallbackTokenName)
  100. }
  101. if got := tokenRow(t, installTokenName); got.Id != installed.Id {
  102. t.Fatalf("%s row id = %d, want %d — the installer's token was replaced", installTokenName, got.Id, installed.Id)
  103. }
  104. if got := tokenRow(t, installTokenName); got.Token != installed.Token {
  105. t.Fatalf("the %s token hash changed, so the recorded credential stopped working", installTokenName)
  106. }
  107. }
  108. // `-getApiToken true -tokenName ci-bot` parses tokenName as "", because flag
  109. // stops at the positional. The command must not then rotate the shared slot.
  110. func TestGetApiTokenWarnsOnIgnoredPositionalArgs(t *testing.T) {
  111. set := flag.NewFlagSet("setting", flag.ContinueOnError)
  112. var getApiToken bool
  113. var tokenName string
  114. set.BoolVar(&getApiToken, "getApiToken", false, "")
  115. set.StringVar(&tokenName, "tokenName", "", "")
  116. if err := set.Parse([]string{"-getApiToken", "true", "-tokenName", "ci-bot"}); err != nil {
  117. t.Fatalf("parse: %v", err)
  118. }
  119. if tokenName != "" {
  120. t.Fatalf("tokenName = %q; this test guards the case where flag drops it", tokenName)
  121. }
  122. if got := set.Args(); len(got) == 0 {
  123. t.Fatal("leftover arguments must be visible so the CLI can warn instead of silently rotating cli-fallback")
  124. }
  125. }
  126. func TestGetApiTokenTrimsName(t *testing.T) {
  127. newTokenCLIEnv(t)
  128. if _, err := (&panel.ApiTokenService{}).RecreateByName("seed", ""); err != nil {
  129. t.Fatalf("seed: %v", err)
  130. }
  131. GetApiToken(true, " ", "")
  132. names := tokenNames(t)
  133. if !hasName(names, cliFallbackTokenName) {
  134. t.Fatalf("token names = %v, want a whitespace-only name to fall back to %s", names, cliFallbackTokenName)
  135. }
  136. }