Skip to content

Commit d3df29f

Browse files
cursoragentfiftin
andcommitted
fix(secrets): delete external secret for non-sync keys on writable storage
Commit 757e9e9 combined read-only and synchronized branches so DeleteSecret was skipped whenever key.Synchronized was true, even when the backend was writable. That left orphaned secrets in Vault when removing user-created keys. Split the conditions: skip external delete only for read-only storage or for synchronized keys; add tests. Co-authored-by: Denis Gukov <fiftin@outlook.com>
1 parent aa231cf commit d3df29f

2 files changed

Lines changed: 97 additions & 6 deletions

File tree

services/server/AccessKey_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,66 @@ func TestSerializeSecretReadOnlyReturnsUnwrappableSentinel(t *testing.T) {
123123
}
124124
}
125125

126+
func TestDeleteCallsExternalBackendForWritableNonSynchronizedKey(t *testing.T) {
127+
vaultType := db.AccessKeySourceStorageVault
128+
projectID := 1
129+
storageID := 10
130+
131+
key := db.AccessKey{
132+
ID: 42,
133+
Name: "vault-key",
134+
Type: db.AccessKeyString,
135+
ProjectID: &projectID,
136+
SourceStorageType: &vaultType,
137+
SourceStorageID: &storageID,
138+
Synchronized: false,
139+
}
140+
141+
repo := &mockAccessKeyRepo{keys: []db.AccessKey{key}}
142+
enc := &mockEncryptionForDelete{}
143+
storages := &mockSecretStorageRepo{
144+
storage: db.SecretStorage{ID: storageID, ReadOnly: false},
145+
}
146+
svc := NewAccessKeyService(repo, enc, storages)
147+
148+
if err := svc.Delete(projectID, key.ID); err != nil {
149+
t.Fatalf("Delete: %v", err)
150+
}
151+
if !enc.deleteSecretCalled {
152+
t.Fatal("expected DeleteSecret on writable non-synchronized key")
153+
}
154+
}
155+
156+
func TestDeleteSkipsExternalBackendForWritableSynchronizedKey(t *testing.T) {
157+
vaultType := db.AccessKeySourceStorageVault
158+
projectID := 1
159+
storageID := 10
160+
161+
key := db.AccessKey{
162+
ID: 43,
163+
Name: "synced-vault-key",
164+
Type: db.AccessKeyString,
165+
ProjectID: &projectID,
166+
SourceStorageType: &vaultType,
167+
SourceStorageID: &storageID,
168+
Synchronized: true,
169+
}
170+
171+
repo := &mockAccessKeyRepo{keys: []db.AccessKey{key}}
172+
enc := &mockEncryptionForDelete{}
173+
storages := &mockSecretStorageRepo{
174+
storage: db.SecretStorage{ID: storageID, ReadOnly: false},
175+
}
176+
svc := NewAccessKeyService(repo, enc, storages)
177+
178+
if err := svc.Delete(projectID, key.ID); err != nil {
179+
t.Fatalf("Delete: %v", err)
180+
}
181+
if enc.deleteSecretCalled {
182+
t.Fatal("expected DeleteSecret to be skipped for synchronized key (local reference only)")
183+
}
184+
}
185+
126186
func TestCreateSkipsSerializationForReadOnlyStorage(t *testing.T) {
127187
storageType := db.AccessKeySourceStorageEnv
128188
key := db.AccessKey{
@@ -231,6 +291,38 @@ func TestRekeyAccessKeysSkipsExternalStorageKeys(t *testing.T) {
231291
func intPtr(i int) *int { return &i }
232292
func strPtr(s string) *string { return &s }
233293

294+
type mockEncryptionForDelete struct {
295+
deleteSecretCalled bool
296+
}
297+
298+
func (m *mockEncryptionForDelete) SerializeSecret(*db.AccessKey) error { return nil }
299+
func (m *mockEncryptionForDelete) DeserializeSecret(*db.AccessKey) error { return nil }
300+
func (m *mockEncryptionForDelete) FillEnvironmentSecrets(*db.Environment, bool) error { return nil }
301+
func (m *mockEncryptionForDelete) DeleteSecret(*db.AccessKey) error {
302+
m.deleteSecretCalled = true
303+
return nil
304+
}
305+
func (m *mockEncryptionForDelete) RekeyAccessKeys(string) error { return nil }
306+
307+
type mockSecretStorageRepo struct {
308+
storage db.SecretStorage
309+
}
310+
311+
func (m *mockSecretStorageRepo) GetSecretStorages(int) ([]db.SecretStorage, error) {
312+
return nil, nil
313+
}
314+
func (m *mockSecretStorageRepo) CreateSecretStorage(db.SecretStorage) (db.SecretStorage, error) {
315+
return db.SecretStorage{}, nil
316+
}
317+
func (m *mockSecretStorageRepo) GetSecretStorage(int, int) (db.SecretStorage, error) {
318+
return m.storage, nil
319+
}
320+
func (m *mockSecretStorageRepo) UpdateSecretStorage(db.SecretStorage) error { return nil }
321+
func (m *mockSecretStorageRepo) GetSecretStorageRefs(int, int) (db.ObjectReferrers, error) {
322+
return db.ObjectReferrers{}, nil
323+
}
324+
func (m *mockSecretStorageRepo) DeleteSecretStorage(int, int) error { return nil }
325+
234326
type mockAccessKeyRepo struct {
235327
keys []db.AccessKey
236328
}

services/server/access_key_svc.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,11 @@ func (s *AccessKeyServiceImpl) Delete(projectID int, keyID int) (err error) {
4545
return
4646
}
4747

48-
if storage.ReadOnly || key.Synchronized {
49-
// Do nothing
50-
51-
//if key.Synchronized {
52-
// err = common_errors.NewUserErrorS("cannot delete synchronized secret from read-only storage")
53-
//}
48+
if storage.ReadOnly {
49+
// Read-only backend: only drop the Semaphore reference, never call the external API.
50+
} else if key.Synchronized {
51+
// Writable backend but this row is a synced mirror of an external secret; do not
52+
// delete the live secret in the vault when removing the local reference.
5453
} else {
5554
err = s.encryptionService.DeleteSecret(&key)
5655
}

0 commit comments

Comments
 (0)