Skip to content

Commit 59ea5b7

Browse files
authored
use separate key for organization list (#1051)
otherwise we overwrite groups info on orgs refresh
2 parents 48add54 + 764e65e commit 59ea5b7

6 files changed

Lines changed: 114 additions & 6 deletions

File tree

internal/cmd/cache.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,11 @@ func invalidateGroupsCache(org string) {
123123
}
124124

125125
const (
126-
ORGS_CACHE_KEY = "organizations"
126+
// ORGS_CACHE_KEY must not share a viper namespace with the per-org group
127+
// cache (orgKey builds "organizations.<org>.groups"). Viper treats dots as
128+
// nested maps, so a plain "organizations" key for the orgs list would
129+
// collide with that nested key and the two caches would clobber each other.
130+
ORGS_CACHE_KEY = "organizations_list"
127131
ORGS_CACHE_TTL_SECONDS = 30 * 60
128132
)
129133

internal/cmd/cache_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,36 @@ func Test_setDbTokenCache_expired(t *testing.T) {
5959
}
6060
}
6161

62+
// Regression test: the orgs-list cache and the per-org groups cache must not
63+
// share a viper namespace, otherwise writing one clobbers the other (viper
64+
// stores dotted keys as nested maps). See ORGS_CACHE_KEY.
65+
func Test_orgsAndGroupsCacheCoexist(t *testing.T) {
66+
orgs := []turso.Organization{{ID: "org-id-123", Slug: "org-a", Type: "team"}}
67+
groups := []turso.Group{{UUID: "group-uuid-abc", Name: "group-a"}}
68+
69+
assertCoexist := func(t *testing.T) {
70+
t.Helper()
71+
if got := getOrgsCache(); len(got) != 1 || got[0].ID != "org-id-123" {
72+
t.Errorf("orgs cache clobbered: getOrgsCache() = %#v", got)
73+
}
74+
if got := getGroupsCache("org-a"); len(got) != 1 || got[0].UUID != "group-uuid-abc" {
75+
t.Errorf("groups cache clobbered: getGroupsCache() = %#v", got)
76+
}
77+
}
78+
79+
t.Run("orgs then groups", func(t *testing.T) {
80+
setOrgsCache(orgs)
81+
setGroupsCache("org-a", groups)
82+
assertCoexist(t)
83+
})
84+
85+
t.Run("groups then orgs", func(t *testing.T) {
86+
setGroupsCache("org-a", groups)
87+
setOrgsCache(orgs)
88+
assertCoexist(t)
89+
})
90+
}
91+
6292
func Test_setLocationsCache(t *testing.T) {
6393
locs := map[string]string{
6494
"ams": "Amsterdam, Netherlands",

internal/cmd/db_generatetoken.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ func getToken(
8181
if database.Group == "" {
8282
return "", errors.New("--group flag can only be set with group databases")
8383
}
84-
return client.Groups.Token(database.Group, expiration, readOnly, claim, fineGrainedPermissions)
84+
return getGroupToken(client, turso.Group{Name: database.Group}, expiration, readOnly, claim, fineGrainedPermissions)
8585
}
8686
if !flags.V3Api() {
8787
return getTokenV2(client, database, expiration, readOnly, claim, fineGrainedPermissions)

internal/cmd/group_tokens.go

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ var groupCreateTokenCmd = &cobra.Command{
127127
if err != nil {
128128
return err
129129
}
130-
token, err := client.Groups.Token(group.Name, expiration, flags.ReadOnly(), claim, permission)
130+
token, err := getGroupToken(client, group, expiration, flags.ReadOnly(), claim, permission)
131131
if err != nil {
132132
return fmt.Errorf("error creating token: %w", err)
133133
}
@@ -137,6 +137,37 @@ var groupCreateTokenCmd = &cobra.Command{
137137
},
138138
}
139139

140+
// getGroupToken creates a group token, using the V3 API when enabled and the
141+
// org/group IDs can be resolved. The V3 endpoint does not support attach
142+
// claims, so requests with a claim fall back to the V2 API.
143+
func getGroupToken(
144+
client *turso.Client,
145+
group turso.Group,
146+
expiration string,
147+
readOnly bool,
148+
claim *turso.PermissionsClaim,
149+
fineGrainedPermissions []flags.FineGrainedPermissions,
150+
) (string, error) {
151+
if !flags.V3Api() || claim != nil {
152+
return client.Groups.Token(group.Name, expiration, readOnly, claim, fineGrainedPermissions)
153+
}
154+
orgID, err := tryResolveOrgID(client)
155+
if err != nil {
156+
return "", err
157+
}
158+
groupID := group.UUID
159+
if groupID == "" {
160+
groupID, err = tryResolveGroupID(client, group.Name)
161+
if err != nil {
162+
return "", err
163+
}
164+
}
165+
if orgID == "" || groupID == "" {
166+
return client.Groups.Token(group.Name, expiration, readOnly, claim, fineGrainedPermissions)
167+
}
168+
return client.GroupsV3.Token(orgID, groupID, expiration, readOnly, fineGrainedPermissions)
169+
}
170+
140171
func validateDBNames(client *turso.Client, dbNames []string) error {
141172
databasesMap, err := getDatabasesMap(client, false)
142173
if err != nil {

internal/cmd/ids.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ import (
55
)
66

77
func tryResolveOrgID(client *turso.Client) (string, error) {
8-
groups := getGroupsCache(client.Org)
9-
108
slug := client.Org
119
if orgs := getOrgsCache(); orgs != nil {
1210
if id := findOrgID(orgs, slug); id != "" {
@@ -18,7 +16,6 @@ func tryResolveOrgID(client *turso.Client) (string, error) {
1816
return "", err
1917
}
2018
setOrgsCache(orgs)
21-
setGroupsCache(client.Org, groups)
2219
return findOrgID(orgs, slug), nil
2320
}
2421

internal/turso/groups_v3.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ package turso
33
import (
44
"fmt"
55
"net/http"
6+
"net/url"
7+
8+
"github.com/tursodatabase/turso-cli/internal/flags"
69
)
710

811
type GroupsV3Client client
@@ -52,3 +55,46 @@ func (g *GroupsV3Client) Get(orgID, groupID string) (Group, error) {
5255
}
5356
return resp.Group, nil
5457
}
58+
59+
func (g *GroupsV3Client) Token(
60+
orgID, groupID string,
61+
expiration string,
62+
readOnly bool,
63+
fineGrainedPermissions []flags.FineGrainedPermissions,
64+
) (string, error) {
65+
q := url.Values{}
66+
if expiration != "" {
67+
q.Set("expiration", expiration)
68+
}
69+
if readOnly {
70+
q.Set("authorization", "read-only")
71+
}
72+
path := g.url(orgID, "/"+groupID+"/auth/tokens")
73+
if enc := q.Encode(); enc != "" {
74+
path += "?" + enc
75+
}
76+
77+
req := GroupTokenRequest{FineGrainedPermissions: fineGrainedPermissions}
78+
body, err := marshal(req)
79+
if err != nil {
80+
return "", fmt.Errorf("could not serialize request body: %w", err)
81+
}
82+
r, err := g.client.Post(path, body)
83+
if err != nil {
84+
return "", fmt.Errorf("failed to get group token: %w", err)
85+
}
86+
defer r.Body.Close()
87+
88+
if r.StatusCode != http.StatusOK {
89+
return "", fmt.Errorf("failed to get group token: %w", parseResponseError(r))
90+
}
91+
92+
type response struct {
93+
Jwt string `json:"jwt"`
94+
}
95+
resp, err := unmarshal[response](r)
96+
if err != nil {
97+
return "", err
98+
}
99+
return resp.Jwt, nil
100+
}

0 commit comments

Comments
 (0)