Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 5 additions & 9 deletions internal/cmd/db_invalidatetokens.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,15 @@ var dbInvalidateTokensCmd = &cobra.Command{
return err
}

if database.Group != "" && database.Version != "tech-preview" {
return fmt.Errorf("database %s is part of group %s, use %s instead", internal.Emph(name), internal.Emph(database.Group), internal.Emph("turso group tokens invalidate <group-name>"))
}

if yesFlag {
return rotateAndNotify(client, database)
}

fmt.Printf("To invalidate %s database tokens, all its replicas must be restarted.\n", internal.Emph(name))
fmt.Printf("All your active connections to the DB will be dropped and there will be a short downtime.\n\n")
fmt.Printf("This will invalidate existing database-scoped tokens for %s. Clients using them will need new tokens.\n", internal.Emph(name))
if database.Group != "" {
fmt.Println("Group tokens will not be invalidated.")
}
fmt.Println()

ok, err := promptConfirmation("Are you sure you want to do this?")
if err != nil {
Expand Down Expand Up @@ -76,8 +75,5 @@ func rotateAndNotify(turso *turso.Client, database turso.Database) error {
func rotate(turso *turso.Client, database turso.Database) error {
invalidateDbTokenCache()
settings.PersistChanges()
if database.Group != "" && database.Version != "tech-preview" {
return turso.Groups.Rotate(database.Group)
}
return turso.Databases.Rotate(database.Name)
}
99 changes: 99 additions & 0 deletions internal/cmd/db_invalidatetokens_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package cmd

import (
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"

"github.com/spf13/viper"
"github.com/tursodatabase/turso-cli/internal/settings"
"github.com/tursodatabase/turso-cli/internal/turso"
)

func TestInvalidateGroupedDatabaseTokensCommand(t *testing.T) {
config, err := settings.ReadSettings()
if err != nil {
t.Fatal(err)
}
previousOrg, previousURL, previousYes := config.Organization(), viper.GetString("baseURL"), yesFlag
t.Cleanup(func() {
config.SetOrganization(previousOrg)
viper.Set("baseURL", previousURL)
yesFlag = previousYes
})
config.SetOrganization("org")
yesFlag = true
t.Setenv(ENV_ACCESS_TOKEN, "test-token")

rotations := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodGet && r.URL.Path == "/v1/organizations/org/databases":
w.Write([]byte(`{"databases":[{"name":"db","group":"group","version":"v1"}]}`))
case r.Method == http.MethodPost && r.URL.Path == "/v1/organizations/org/databases/db/auth/rotate":
rotations++
w.WriteHeader(http.StatusOK)
default:
t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
http.NotFound(w, r)
}
}))
defer server.Close()
viper.Set("baseURL", server.URL)

if err := dbInvalidateTokensCmd.RunE(dbInvalidateTokensCmd, []string{"db"}); err != nil {
t.Fatal(err)
}
if rotations != 1 {
t.Errorf("database rotations = %d, want 1", rotations)
}
}

func TestRotateDatabaseTokens(t *testing.T) {
for _, tc := range []struct {
name string
database turso.Database
status int
wantErr string
}{
{"grouped database", turso.Database{Name: "db", Group: "group", Version: "v1"}, http.StatusOK, ""},
{"tech-preview grouped database", turso.Database{Name: "db", Group: "group", Version: "tech-preview"}, http.StatusOK, ""},
{"ungrouped database", turso.Database{Name: "db"}, http.StatusOK, ""},
{"legacy dedicated grouped database", turso.Database{Name: "db", Group: "group", Version: "v1"}, http.StatusBadRequest, "cannot rotate credentials for a single database in a group"},
} {
t.Run(tc.name, func(t *testing.T) {
requests := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requests++
if r.Method != http.MethodPost || r.URL.Path != "/v1/organizations/org/databases/db/auth/rotate" {
t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
http.NotFound(w, r)
return
}
w.WriteHeader(tc.status)
if tc.status != http.StatusOK {
w.Write([]byte(`{"error":"cannot rotate credentials for a single database in a group"}`))
}
}))
defer server.Close()
base, err := url.Parse(server.URL)
if err != nil {
t.Fatal(err)
}

err = rotate(turso.New(base, "token", "dev", "org"), tc.database)
if requests != 1 {
t.Errorf("requests = %d, want 1", requests)
}
if tc.wantErr == "" {
if err != nil {
t.Fatalf("rotate: %v", err)
}
} else if err == nil || !strings.Contains(err.Error(), tc.wantErr) {
t.Fatalf("rotate error = %v, want %q", err, tc.wantErr)
}
})
}
}
13 changes: 7 additions & 6 deletions internal/cmd/group_tokens.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ var groupTokensCmd = &cobra.Command{

func init() {
groupTokensCmd.AddCommand(groupTokensInvalidateCmd)
flags.AddYes(groupTokensInvalidateCmd, "Confirms the invalidation of the credentials of the group and all its databases")
flags.AddYes(groupTokensInvalidateCmd, "Confirms the rotation of group credentials")
}

var groupTokensInvalidateCmd = &cobra.Command{
Use: "invalidate <group-name>",
Short: "Rotates the keys used to create and verify database tokens, invalidating all existing tokens invalid for the group.",
Short: "Rotates group credentials; database-scoped tokens may need separate invalidation",
Args: cobra.ExactArgs(1),
ValidArgsFunction: groupArg,
RunE: func(cmd *cobra.Command, args []string) error {
Expand All @@ -49,8 +49,8 @@ var groupTokensInvalidateCmd = &cobra.Command{
return rotateGroup(client, group)
}

fmt.Printf("To invalidate tokens for group %s, tokens from all its databases will be invalidated.\n", internal.Emph(name))
fmt.Printf("All your active connections to the databases in that group will be dropped and there will be a short downtime.\n\n")
fmt.Printf("This will rotate credentials for group %s; this invalidates group tokens but not necessarily database-scoped tokens.\n", internal.Emph(name))
fmt.Printf("To invalidate database-scoped tokens, run %s for each database.\n\n", internal.Emph("turso db tokens invalidate <database-name>"))

ok, err := promptConfirmation("Are you sure you want to do this?")
if err != nil {
Expand Down Expand Up @@ -78,8 +78,9 @@ func rotateGroup(turso *turso.Client, group turso.Group) error {
}

s.Stop()
fmt.Printf("✔ Success! Tokens invalidated successfully.\n\n")
fmt.Printf("Run %s to get a new one.\n", internal.Emph("turso group tokens create <group-name>"))
fmt.Printf("✔ Success! Group credentials rotated. This invalidates group tokens but not necessarily database-scoped tokens.\n\n")
fmt.Printf("Run %s to get a new group token.\n", internal.Emph("turso group tokens create <group-name>"))
fmt.Printf("To invalidate database-scoped tokens, run %s for each database.\n", internal.Emph("turso db tokens invalidate <database-name>"))
return nil
}

Expand Down
Loading