Skip to content

Commit 02aec8d

Browse files
committed
Keep the concurrent-save fix scoped to aura.go/credentials.go
1 parent 8465a4f commit 02aec8d

8 files changed

Lines changed: 134 additions & 147 deletions

File tree

common/clicfg/credentials/aura.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,21 @@ import (
1414
type AuraCredentials struct {
1515
DefaultCredential string `json:"default-credential"`
1616
Credentials []*AuraCredential `json:"credentials"`
17+
refresh func() error
18+
persist func() error
19+
}
20+
21+
// refreshAndPersist re-reads the on-disk state into c immediately before mutate runs, so mutate
22+
// always applies its delta on top of the current file rather than a snapshot that may have gone
23+
// stale since load. It only persists if mutate succeeds.
24+
func (c *AuraCredentials) refreshAndPersist(mutate func() error) error {
25+
if err := c.refresh(); err != nil {
26+
return err
27+
}
28+
if err := mutate(); err != nil {
29+
return err
30+
}
31+
return c.persist()
1732
}
1833

1934
func (c *AuraCredentials) List() []*AuraCredential {
@@ -31,6 +46,57 @@ func (config *AuraCredentials) Print(writer io.Writer) error {
3146
return nil
3247
}
3348

49+
func (c *AuraCredentials) Add(name string, clientId string, clientSecret string) error {
50+
return c.refreshAndPersist(func() error {
51+
for _, credential := range c.Credentials {
52+
if credential.Name == name {
53+
return clierr.NewUsageError("already have credential with name %s", name)
54+
}
55+
}
56+
57+
c.Credentials = append(c.Credentials, &AuraCredential{Name: name, ClientId: clientId, ClientSecret: clientSecret})
58+
if len(c.Credentials) == 1 {
59+
c.DefaultCredential = name
60+
}
61+
return nil
62+
})
63+
}
64+
65+
func (c *AuraCredentials) Remove(name string) error {
66+
return c.refreshAndPersist(func() error {
67+
var indexToRemove = -1
68+
69+
for i, credential := range c.Credentials {
70+
if credential.Name == name {
71+
indexToRemove = i
72+
break
73+
}
74+
}
75+
76+
if indexToRemove == -1 {
77+
return clierr.NewUsageError("could not find credential with name %s to remove", name)
78+
}
79+
80+
if c.DefaultCredential == name {
81+
c.DefaultCredential = ""
82+
}
83+
84+
c.Credentials = append(c.Credentials[:indexToRemove], c.Credentials[indexToRemove+1:]...)
85+
return nil
86+
})
87+
}
88+
89+
func (c *AuraCredentials) SetDefault(name string) error {
90+
return c.refreshAndPersist(func() error {
91+
if !c.credentialExists(name) {
92+
return clierr.NewUsageError("could not find credential with name %s", name)
93+
}
94+
95+
c.DefaultCredential = name
96+
return nil
97+
})
98+
}
99+
34100
func (c *AuraCredentials) GetDefault() (*AuraCredential, error) {
35101
if c.DefaultCredential == "" {
36102
return nil, clierr.NewUsageError("default credential not set, please follow the instructions at https://neo4j.com/docs/aura/classic/platform/api/authentication/#_creating_credentials and use the `credential add` subcommand to add the created credentials")
@@ -47,6 +113,47 @@ func (c *AuraCredentials) Get(name string) (*AuraCredential, error) {
47113
return nil, clierr.NewUsageError("could not find credential with name %s", name)
48114
}
49115

116+
func (c *AuraCredentials) UpdateAccessToken(cred *AuraCredential, accessToken string, expiresInSeconds int64) *AuraCredential {
117+
var credential *AuraCredential
118+
err := c.refreshAndPersist(func() error {
119+
var err error
120+
credential, err = c.Get(cred.Name)
121+
if err != nil {
122+
return err
123+
}
124+
125+
const expireToleranceSeconds = 60
126+
now := time.Now().UnixMilli()
127+
128+
credential.TokenExpiry = now + (expiresInSeconds-expireToleranceSeconds)*1000
129+
credential.AccessToken = accessToken
130+
return nil
131+
})
132+
if err != nil {
133+
panic(err)
134+
}
135+
return credential
136+
}
137+
138+
func (c *AuraCredentials) ClearAccessToken(cred *AuraCredential) (*AuraCredential, error) {
139+
var credential *AuraCredential
140+
err := c.refreshAndPersist(func() error {
141+
var err error
142+
credential, err = c.Get(cred.Name)
143+
if err != nil {
144+
return err
145+
}
146+
147+
credential.TokenExpiry = 0
148+
credential.AccessToken = ""
149+
return nil
150+
})
151+
if err != nil {
152+
return nil, err
153+
}
154+
return credential, nil
155+
}
156+
50157
func (c *AuraCredentials) credentialExists(name string) bool {
51158
for _, credential := range c.Credentials {
52159
if credential.Name == name {

common/clicfg/credentials/credentials.go

Lines changed: 17 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,8 @@ package credentials
66
import (
77
"encoding/json"
88
"path/filepath"
9-
"time"
109

1110
"github.com/neo4j/cli/common/clicfg/fileutils"
12-
"github.com/neo4j/cli/common/clierr"
1311
"github.com/spf13/afero"
1412
)
1513

@@ -49,164 +47,46 @@ func (c *Credentials) load() {
4947
}
5048

5149
c.Aura = credentials.Aura
50+
c.Aura.refresh = c.refreshAura
51+
c.Aura.persist = c.save
5252

5353
if !fileHasData {
54-
c.writeAura(c.Aura)
54+
c.save()
5555
}
5656
}
5757

58-
func (c *Credentials) readAuraFresh() (*AuraCredentials, error) {
58+
// refreshAura re-reads the credentials file from disk into the existing c.Aura, discarding
59+
// whatever was loaded or mutated in memory before this call. It updates the struct in place
60+
// rather than replacing it, so the refresh/persist closures wired up in load stay intact.
61+
func (c *Credentials) refreshAura() error {
5962
data := fileutils.ReadFileSafe(c.fs, c.filePath)
6063

6164
var credFile CredentialsFile
62-
if len(data) == 0 {
63-
credFile = CredentialsFile{
64-
Aura: &AuraCredentials{
65-
Credentials: []*AuraCredential{},
66-
},
67-
}
68-
} else {
65+
if len(data) != 0 {
6966
if err := json.Unmarshal(data, &credFile); err != nil {
70-
return nil, err
67+
return err
7168
}
7269
}
7370

7471
if credFile.Aura == nil {
75-
credFile.Aura = &AuraCredentials{
76-
Credentials: []*AuraCredential{},
77-
}
72+
c.Aura.Credentials = []*AuraCredential{}
73+
c.Aura.DefaultCredential = ""
74+
return nil
7875
}
7976

80-
return credFile.Aura, nil
77+
c.Aura.Credentials = credFile.Aura.Credentials
78+
c.Aura.DefaultCredential = credFile.Aura.DefaultCredential
79+
return nil
8180
}
8281

83-
func (c *Credentials) writeAura(aura *AuraCredentials) error {
82+
func (c *Credentials) save() error {
8483
data, err := json.Marshal(CredentialsFile{
85-
Aura: aura,
84+
Aura: c.Aura,
8685
})
8786
if err != nil {
8887
panic(err)
8988
}
9089

9190
fileutils.WriteFile(c.fs, c.filePath, data)
92-
c.Aura = aura
9391
return nil
9492
}
95-
96-
func (c *Credentials) Add(name string, clientId string, clientSecret string) error {
97-
aura, err := c.readAuraFresh()
98-
if err != nil {
99-
return err
100-
}
101-
102-
for _, credential := range aura.Credentials {
103-
if credential.Name == name {
104-
return clierr.NewUsageError("already have credential with name %s", name)
105-
}
106-
}
107-
108-
aura.Credentials = append(aura.Credentials, &AuraCredential{Name: name, ClientId: clientId, ClientSecret: clientSecret})
109-
if len(aura.Credentials) == 1 {
110-
aura.DefaultCredential = name
111-
}
112-
113-
return c.writeAura(aura)
114-
}
115-
116-
func (c *Credentials) Remove(name string) error {
117-
aura, err := c.readAuraFresh()
118-
if err != nil {
119-
return err
120-
}
121-
122-
var indexToRemove = -1
123-
for i, credential := range aura.Credentials {
124-
if credential.Name == name {
125-
indexToRemove = i
126-
break
127-
}
128-
}
129-
130-
if indexToRemove == -1 {
131-
return clierr.NewUsageError("could not find credential with name %s to remove", name)
132-
}
133-
134-
if aura.DefaultCredential == name {
135-
aura.DefaultCredential = ""
136-
}
137-
138-
aura.Credentials = append(aura.Credentials[:indexToRemove], aura.Credentials[indexToRemove+1:]...)
139-
140-
return c.writeAura(aura)
141-
}
142-
143-
func (c *Credentials) SetDefault(name string) error {
144-
aura, err := c.readAuraFresh()
145-
if err != nil {
146-
return err
147-
}
148-
149-
if !c.credentialExists(name, aura) {
150-
return clierr.NewUsageError("could not find credential with name %s", name)
151-
}
152-
153-
aura.DefaultCredential = name
154-
155-
return c.writeAura(aura)
156-
}
157-
158-
func (c *Credentials) UpdateAccessToken(cred *AuraCredential, accessToken string, expiresInSeconds int64) *AuraCredential {
159-
aura, err := c.readAuraFresh()
160-
if err != nil {
161-
panic(err)
162-
}
163-
164-
credential, err := aura.Get(cred.Name)
165-
if err != nil {
166-
panic(err)
167-
}
168-
169-
const expireToleranceSeconds = 60
170-
now := time.Now().UnixMilli()
171-
172-
credential.TokenExpiry = now + (expiresInSeconds-expireToleranceSeconds)*1000
173-
credential.AccessToken = accessToken
174-
175-
err = c.writeAura(aura)
176-
if err != nil {
177-
panic(err)
178-
}
179-
180-
return credential
181-
}
182-
183-
func (c *Credentials) ClearAccessToken(cred *AuraCredential) (*AuraCredential, error) {
184-
aura, err := c.readAuraFresh()
185-
if err != nil {
186-
return nil, err
187-
}
188-
189-
credential, err := aura.Get(cred.Name)
190-
if err != nil {
191-
return nil, err
192-
}
193-
194-
credential.TokenExpiry = 0
195-
credential.AccessToken = ""
196-
197-
err = c.writeAura(aura)
198-
if err != nil {
199-
return nil, err
200-
}
201-
202-
return credential, nil
203-
}
204-
205-
func (c *Credentials) credentialExists(name string, aura *AuraCredentials) bool {
206-
for _, credential := range aura.Credentials {
207-
if credential.Name == name {
208-
return true
209-
}
210-
}
211-
return false
212-
}

common/clicfg/credentials/credentials_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@ func TestConcurrentSaveDoesNotLoseAnEarlierWrite(t *testing.T) {
2121
processB := credentials.NewCredentials(fs, configPrefix)
2222

2323
// Process A finishes first: `credential add --name first ...`.
24-
assert.NoError(t, processA.Add("first", "client-a", "secret-a"))
24+
assert.NoError(t, processA.Aura.Add("first", "client-a", "secret-a"))
2525

2626
// Process B finishes second — e.g. a routine token refresh triggering a
2727
// save as a side effect, or a second `credential add`. It never re-read
2828
// the file, so its in-memory view still thinks the store is empty.
29-
assert.NoError(t, processB.Add("second", "client-b", "secret-b"))
29+
assert.NoError(t, processB.Aura.Add("second", "client-b", "secret-b"))
3030

3131
// A fresh read of the file is what the next command would see.
3232
onDisk := credentials.NewCredentials(fs, configPrefix)
@@ -48,13 +48,13 @@ func TestRemoveThenReaddSameNameWithinProcessDoesNotLoseCredential(t *testing.T)
4848
cli := credentials.NewCredentials(fs, configPrefix)
4949

5050
// 2. Adds a credential
51-
assert.NoError(t, cli.Add("test", "client-id", "client-secret"))
51+
assert.NoError(t, cli.Aura.Add("test", "client-id", "client-secret"))
5252

5353
// 3. Removes it
54-
assert.NoError(t, cli.Remove("test"))
54+
assert.NoError(t, cli.Aura.Remove("test"))
5555

5656
// 4. Re-adds it with the same name (but new client ID/secret)
57-
assert.NoError(t, cli.Add("test", "new-client-id", "new-client-secret"))
57+
assert.NoError(t, cli.Aura.Add("test", "new-client-id", "new-client-secret"))
5858

5959
// 5. A fresh read of the file should have the credential
6060
onDisk := credentials.NewCredentials(fs, configPrefix)

neo4j-cli/aura/internal/api/response.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ func formatAuthorizationError(resBody []byte, statusCode int, credential *creden
352352
messages = append(messages, e.Message)
353353
}
354354

355-
_, err = cfg.Credentials.ClearAccessToken(credential)
355+
_, err = cfg.Credentials.Aura.ClearAccessToken(credential)
356356
if err != nil {
357357
messages = append(messages, "Request failed authorization - attempted to clear the access token but encountered an error, please report an issue in https://github.com/neo4j/cli")
358358
} else {

neo4j-cli/aura/internal/api/token.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,6 @@ func getToken(credential *credentials.AuraCredential, cfg *clicfg.Config) (strin
6969
panic(clierr.NewFatalError("can't retrieve authentication token. %w", err))
7070
}
7171

72-
cfg.Credentials.UpdateAccessToken(credential, grant.AccessToken, grant.ExpiresIn)
72+
cfg.Credentials.Aura.UpdateAccessToken(credential, grant.AccessToken, grant.ExpiresIn)
7373
return grant.AccessToken, err
7474
}

neo4j-cli/aura/internal/subcommands/credential/add.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ func NewAddCmd(cfg *clicfg.Config) *cobra.Command {
2525
Use: "add",
2626
Short: "Adds a credential",
2727
RunE: func(cmd *cobra.Command, args []string) error {
28-
return cfg.Credentials.Add(name, clientId, clientSecret)
28+
return cfg.Credentials.Aura.Add(name, clientId, clientSecret)
2929
},
3030
}
3131

neo4j-cli/aura/internal/subcommands/credential/remove.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ func NewRemoveCmd(cfg *clicfg.Config) *cobra.Command {
1414
Short: "Removes a credential",
1515
Args: cobra.ExactArgs(1),
1616
RunE: func(cmd *cobra.Command, args []string) error {
17-
return cfg.Credentials.Remove(args[0])
17+
return cfg.Credentials.Aura.Remove(args[0])
1818
},
1919
}
2020
}

neo4j-cli/aura/internal/subcommands/credential/use.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ func NewUseCmd(cfg *clicfg.Config) *cobra.Command {
1414
Short: "Sets the default credential to be used",
1515
Args: cobra.ExactArgs(1),
1616
RunE: func(cmd *cobra.Command, args []string) error {
17-
return cfg.Credentials.SetDefault(args[0])
17+
return cfg.Credentials.Aura.SetDefault(args[0])
1818
},
1919
}
2020
}

0 commit comments

Comments
 (0)