Skip to content

Commit 6291546

Browse files
committed
feat: upsert credential support
On macOS the credentials cannot be stored if one already exists. But we didn't want to clobber the credentials on Save. A new `Upsert` function is added to clobber credentials purposefully. macOS is the only operating system that requires this to be atomic, but others might require it too. Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
1 parent 75256c1 commit 6291546

7 files changed

Lines changed: 56 additions & 0 deletions

File tree

plugins/pass/teststore/teststore.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ type MockStore struct {
3333
errDelete error
3434
errGet error
3535
errFilter error
36+
errUpsert error
3637
store map[store.ID]store.Secret
3738
}
3839

@@ -74,6 +75,12 @@ func WithStoreDeleteErr(err error) Option {
7475
}
7576
}
7677

78+
func WithStoreUpsertErr(err error) Option {
79+
return func(m *MockStore) {
80+
m.errUpsert = err
81+
}
82+
}
83+
7784
func WithStore(store map[store.ID]store.Secret) Option {
7885
return func(m *MockStore) {
7986
m.store = store
@@ -127,6 +134,17 @@ func (m *MockStore) Save(_ context.Context, id store.ID, secret store.Secret) er
127134
return nil
128135
}
129136

137+
func (m *MockStore) Upsert(_ context.Context, id store.ID, secret store.Secret) error {
138+
m.lock.Lock()
139+
defer m.lock.Unlock()
140+
if m.errUpsert != nil {
141+
return m.errUpsert
142+
}
143+
144+
m.store[id] = secret
145+
return nil
146+
}
147+
130148
func (m *MockStore) Filter(_ context.Context, pattern store.Pattern) (map[store.ID]store.Secret, error) {
131149
m.lock.Lock()
132150
defer m.lock.Unlock()

store/keychain/keychain_darwin.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"errors"
2020
"fmt"
2121
"maps"
22+
"sync"
2223

2324
kc "github.com/docker/secrets-engine/store/keychain/internal/go-keychain"
2425

@@ -31,6 +32,7 @@ var (
3132
)
3233

3334
type keychainStore[T store.Secret] struct {
35+
mu sync.Mutex
3436
serviceGroup string
3537
serviceName string
3638
factory store.Factory[T]
@@ -198,6 +200,21 @@ func (k *keychainStore[T]) Save(_ context.Context, id store.ID, secret store.Sec
198200
return mapKeychainError(kc.AddItem(item))
199201
}
200202

203+
// Upsert atomically replaces a credential in the macOS Keychain.
204+
//
205+
// The macOS Keychain does not allow overwriting an existing item via AddItem,
206+
// so Upsert holds a mutex and performs a Delete followed by a Save to ensure
207+
// no concurrent Upsert can interleave between the two operations.
208+
func (k *keychainStore[T]) Upsert(ctx context.Context, id store.ID, secret store.Secret) error {
209+
k.mu.Lock()
210+
defer k.mu.Unlock()
211+
212+
if err := k.Delete(ctx, id); err != nil {
213+
return err
214+
}
215+
return k.Save(ctx, id, secret)
216+
}
217+
201218
func (k *keychainStore[T]) Filter(ctx context.Context, pattern store.Pattern) (map[store.ID]store.Secret, error) {
202219
// Note: Filter on macOS cannot filter by generic attributes and thus we
203220
// cannot split the ID and store it in the keychain as parts for later

store/keychain/keychain_linux.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,10 @@ func (k *keychainStore[T]) Save(_ context.Context, id store.ID, secret store.Sec
339339
return nil
340340
}
341341

342+
func (k *keychainStore[T]) Upsert(ctx context.Context, id store.ID, secret store.Secret) error {
343+
return k.Save(ctx, id, secret)
344+
}
345+
342346
//gocyclo:ignore
343347
func (k *keychainStore[T]) Filter(ctx context.Context, pattern store.Pattern) (map[store.ID]store.Secret, error) {
344348
service, err := kc.NewService()

store/keychain/keychain_windows.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,10 @@ func (k *keychainStore[T]) Save(_ context.Context, id store.ID, secret store.Sec
336336
return mapWindowsCredentialError(g.Write())
337337
}
338338

339+
func (k *keychainStore[T]) Upsert(ctx context.Context, id store.ID, secret store.Secret) error {
340+
return k.Save(ctx, id, secret)
341+
}
342+
339343
func (k *keychainStore[T]) Filter(ctx context.Context, pattern store.Pattern) (map[store.ID]store.Secret, error) {
340344
// Note: there is no notion of a filter on Windows inside the wincred API.
341345
// It has no way to even filter on known attributes.

store/mocks/mock_store.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ func (m *MockStore) Save(_ context.Context, id store.ID, secret store.Secret) er
7070
return nil
7171
}
7272

73+
func (m *MockStore) Upsert(ctx context.Context, id store.ID, secret store.Secret) error {
74+
return m.Save(ctx, id, secret)
75+
}
76+
7377
func (m *MockStore) Filter(_ context.Context, pattern store.Pattern) (map[store.ID]store.Secret, error) {
7478
m.lock.Lock()
7579
defer m.lock.Unlock()

store/posixage/store.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,10 @@ func (f *fileStore[T]) Save(ctx context.Context, id store.ID, s store.Secret) er
376376
return secretfile.Persist(id, f.filesystem, metadata, secrets)
377377
}
378378

379+
func (f *fileStore[T]) Upsert(ctx context.Context, id store.ID, s store.Secret) error {
380+
return f.Save(ctx, id, s)
381+
}
382+
379383
type config struct {
380384
logger logging.Logger
381385
registeredDecryptionFunc []promptCaller

store/store.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@ type Store interface {
8484
GetAllMetadata(ctx context.Context) (map[ID]Secret, error)
8585
// Save persists credentials from the store.
8686
Save(ctx context.Context, id ID, secret Secret) error
87+
// Upsert atomically replaces an existing credential or inserts a new one.
88+
// On stores that do not support overwriting (e.g. macOS Keychain), it
89+
// deletes the existing credential and then saves the new one under a mutex
90+
// to ensure the two operations are not interleaved.
91+
Upsert(ctx context.Context, id ID, secret Secret) error
8792
// Filter returns a map of secrets based on a [Pattern].
8893
//
8994
// Secrets returned will have both [Secret.SetMetadata] and [Secret.Unmarshal]

0 commit comments

Comments
 (0)