From ed17120c478901383121f8ce6706894eea835301 Mon Sep 17 00:00:00 2001 From: Bharat Kathi Date: Fri, 26 Jun 2026 00:21:42 -0700 Subject: [PATCH] feat: hard-delete instead of soft-delete; rename app/account secret tables Drop DeletedAt from Account/Application/AppSecret/Secret and switch all deletes to actual row removals (gorm `Delete`). Cascade Account->Secret deletes in a transaction the same way Application->AppSecret already does. Rename the secret tables for consistency with their owner: vault_secret -> vault_account_secret, vault_app_secret -> vault_application_secret. Tables that already match the convention (vault_account, vault_application) are unchanged. Pre-deploy SQL migration is in the PR body. --- vault/api/account.go | 2 +- vault/api/app_secret.go | 4 ++-- vault/api/secret.go | 2 +- vault/model/account.go | 19 ++++++++--------- vault/model/app_secret.go | 29 +++++++++++++------------- vault/model/application.go | 3 +-- vault/model/secret.go | 35 +++++++++++++++---------------- vault/service/account.go | 28 ++++++++++--------------- vault/service/app_secret.go | 41 +++++++++---------------------------- vault/service/secret.go | 16 +++++---------- 10 files changed, 71 insertions(+), 108 deletions(-) diff --git a/vault/api/account.go b/vault/api/account.go index 552a936..b328dd5 100644 --- a/vault/api/account.go +++ b/vault/api/account.go @@ -132,7 +132,7 @@ func DeleteAccount(c *gin.Context) { return } Require(c, RequestTokenCanAccessAccount(c, account)) - if err := service.DeleteAccountWithAudit(account, GetRequestEntityID(c), newAccountAuditLog(c, service.AuditActionAccountDeleted, account)); err != nil { + if err := service.DeleteAccountWithAudit(account, newAccountAuditLog(c, service.AuditActionAccountDeleted, account)); err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "account not found"}) return diff --git a/vault/api/app_secret.go b/vault/api/app_secret.go index a7fc4c4..0b5ecf8 100644 --- a/vault/api/app_secret.go +++ b/vault/api/app_secret.go @@ -113,7 +113,7 @@ func DeleteApplication(c *gin.Context) { return } Require(c, RequestTokenCanAccessApplication(c, application)) - if err := service.DeleteApplication(application, GetRequestEntityID(c)); err != nil { + if err := service.DeleteApplication(application); err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "application not found"}) return @@ -208,7 +208,7 @@ func DeleteApplicationSecret(c *gin.Context) { return } Require(c, RequestTokenCanAccessApplication(c, application)) - if err := service.DeleteAppSecret(application.ID, c.Param("secretID"), GetRequestEntityID(c)); err != nil { + if err := service.DeleteAppSecret(application.ID, c.Param("secretID")); err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "app secret not found"}) return diff --git a/vault/api/secret.go b/vault/api/secret.go index e38c4e9..040a799 100644 --- a/vault/api/secret.go +++ b/vault/api/secret.go @@ -181,7 +181,7 @@ func DeleteSecret(c *gin.Context) { } Require(c, RequestTokenCanAccessAccount(c, account)) - if err := service.DeleteSecret(c.Param("id"), c.Param("secretID"), GetRequestEntityID(c)); err != nil { + if err := service.DeleteSecret(c.Param("id"), c.Param("secretID")); err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "secret not found"}) return diff --git a/vault/model/account.go b/vault/model/account.go index 69ef7c6..9067a3b 100644 --- a/vault/model/account.go +++ b/vault/model/account.go @@ -3,16 +3,15 @@ package model import "time" type Account struct { - ID string `json:"id" gorm:"primaryKey"` - Name string `json:"name" gorm:"index"` - Description string `json:"description"` - URL string `json:"url"` - AccessGroupNames []string `json:"access_group_names" gorm:"type:jsonb;serializer:json"` - CreatedByEntityID string `json:"created_by_entity_id" gorm:"index"` - UpdatedByEntityID string `json:"updated_by_entity_id" gorm:"index"` - DeletedAt *time.Time `json:"deleted_at" gorm:"index"` - CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` - UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"` + ID string `json:"id" gorm:"primaryKey"` + Name string `json:"name" gorm:"index"` + Description string `json:"description"` + URL string `json:"url"` + AccessGroupNames []string `json:"access_group_names" gorm:"type:jsonb;serializer:json"` + CreatedByEntityID string `json:"created_by_entity_id" gorm:"index"` + UpdatedByEntityID string `json:"updated_by_entity_id" gorm:"index"` + CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` + UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"` } func (Account) TableName() string { diff --git a/vault/model/app_secret.go b/vault/model/app_secret.go index ed126e6..7bd99f7 100644 --- a/vault/model/app_secret.go +++ b/vault/model/app_secret.go @@ -3,22 +3,21 @@ package model import "time" type AppSecret struct { - ID string `json:"id" gorm:"primaryKey"` - ApplicationID string `json:"application_id" gorm:"index;index:idx_app_secret_application_key_live,unique,where:deleted_at IS NULL"` - Key string `json:"key" gorm:"index:idx_app_secret_application_key_live,unique,where:deleted_at IS NULL"` - Ciphertext []byte `json:"-" gorm:"type:bytea"` - Nonce []byte `json:"-" gorm:"type:bytea"` - EncryptedDataKey []byte `json:"-" gorm:"type:bytea"` - KeyID string `json:"key_id" gorm:"index"` - Algorithm string `json:"algorithm"` - CreatedByEntityID string `json:"created_by_entity_id" gorm:"index"` - UpdatedByEntityID string `json:"updated_by_entity_id" gorm:"index"` - DeletedAt *time.Time `json:"deleted_at" gorm:"index"` - CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` - UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"` - PlainValue string `json:"-" gorm:"-"` + ID string `json:"id" gorm:"primaryKey"` + ApplicationID string `json:"application_id" gorm:"index;uniqueIndex:idx_app_secret_application_key"` + Key string `json:"key" gorm:"uniqueIndex:idx_app_secret_application_key"` + Ciphertext []byte `json:"-" gorm:"type:bytea"` + Nonce []byte `json:"-" gorm:"type:bytea"` + EncryptedDataKey []byte `json:"-" gorm:"type:bytea"` + KeyID string `json:"key_id" gorm:"index"` + Algorithm string `json:"algorithm"` + CreatedByEntityID string `json:"created_by_entity_id" gorm:"index"` + UpdatedByEntityID string `json:"updated_by_entity_id" gorm:"index"` + CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` + UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"` + PlainValue string `json:"-" gorm:"-"` } func (AppSecret) TableName() string { - return "vault_app_secret" + return "vault_application_secret" } diff --git a/vault/model/application.go b/vault/model/application.go index 4d4d6b8..95c0fff 100644 --- a/vault/model/application.go +++ b/vault/model/application.go @@ -4,11 +4,10 @@ import "time" type Application struct { ID string `json:"id" gorm:"primaryKey"` - Name string `json:"name" gorm:"index:idx_application_name_live,unique,where:deleted_at IS NULL"` + Name string `json:"name" gorm:"uniqueIndex"` AccessGroupNames []string `json:"access_group_names" gorm:"type:jsonb;serializer:json"` CreatedByEntityID string `json:"created_by_entity_id" gorm:"index"` UpdatedByEntityID string `json:"updated_by_entity_id" gorm:"index"` - DeletedAt *time.Time `json:"deleted_at" gorm:"index"` CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"` Secrets []AppSecret `json:"secrets,omitempty" gorm:"foreignKey:ApplicationID"` diff --git a/vault/model/secret.go b/vault/model/secret.go index a7f7bba..b48d3fd 100644 --- a/vault/model/secret.go +++ b/vault/model/secret.go @@ -3,25 +3,24 @@ package model import "time" type Secret struct { - ID string `json:"id" gorm:"primaryKey"` - AccountID string `json:"account_id" gorm:"index;uniqueIndex:idx_secret_account_key"` - Key string `json:"key" gorm:"uniqueIndex:idx_secret_account_key"` - Label string `json:"label"` - Type string `json:"type" gorm:"index"` - Sensitive bool `json:"sensitive" gorm:"index"` - PlainValue string `json:"plain_value"` - Ciphertext []byte `json:"-" gorm:"type:bytea"` - Nonce []byte `json:"-" gorm:"type:bytea"` - EncryptedDataKey []byte `json:"-" gorm:"type:bytea"` - KeyID string `json:"key_id" gorm:"index"` - Algorithm string `json:"algorithm"` - CreatedByEntityID string `json:"created_by_entity_id" gorm:"index"` - UpdatedByEntityID string `json:"updated_by_entity_id" gorm:"index"` - DeletedAt *time.Time `json:"deleted_at" gorm:"index"` - CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` - UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"` + ID string `json:"id" gorm:"primaryKey"` + AccountID string `json:"account_id" gorm:"index;uniqueIndex:idx_secret_account_key"` + Key string `json:"key" gorm:"uniqueIndex:idx_secret_account_key"` + Label string `json:"label"` + Type string `json:"type" gorm:"index"` + Sensitive bool `json:"sensitive" gorm:"index"` + PlainValue string `json:"plain_value"` + Ciphertext []byte `json:"-" gorm:"type:bytea"` + Nonce []byte `json:"-" gorm:"type:bytea"` + EncryptedDataKey []byte `json:"-" gorm:"type:bytea"` + KeyID string `json:"key_id" gorm:"index"` + Algorithm string `json:"algorithm"` + CreatedByEntityID string `json:"created_by_entity_id" gorm:"index"` + UpdatedByEntityID string `json:"updated_by_entity_id" gorm:"index"` + CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` + UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"` } func (Secret) TableName() string { - return "vault_secret" + return "vault_account_secret" } diff --git a/vault/service/account.go b/vault/service/account.go index d0a802a..89829cc 100644 --- a/vault/service/account.go +++ b/vault/service/account.go @@ -3,7 +3,6 @@ package service import ( "errors" "strings" - "time" "github.com/gaucho-racing/ulid-go" "github.com/gaucho-racing/vault/vault/database" @@ -31,7 +30,6 @@ type accountSecretCount struct { func GetAllAccounts() ([]AccountWithSecretCount, error) { accounts := []model.Account{} if err := database.DB. - Where("deleted_at IS NULL"). Order("name ASC"). Find(&accounts).Error; err != nil { return []AccountWithSecretCount{}, err @@ -59,7 +57,7 @@ func GetAllAccounts() ([]AccountWithSecretCount, error) { func GetAccountByID(id string) (model.Account, error) { var account model.Account - if err := database.DB.Where("id = ? AND deleted_at IS NULL", id).First(&account).Error; err != nil { + if err := database.DB.Where("id = ?", id).First(&account).Error; err != nil { return model.Account{}, err } return account, nil @@ -87,7 +85,7 @@ func getSecretCountsByAccountID(accountIDs []string) (map[string]int64, error) { if err := database.DB. Model(&model.Secret{}). Select("account_id, count(*) as secret_count"). - Where("account_id IN ? AND deleted_at IS NULL", accountIDs). + Where("account_id IN ?", accountIDs). Group("account_id"). Scan(&counts).Error; err != nil { return map[string]int64{}, err @@ -162,13 +160,13 @@ func updateAccount(db *gorm.DB, account model.Account) (model.Account, error) { return account, nil } -func DeleteAccount(id string, entityID string) error { - return deleteAccount(database.DB, id, entityID) +func DeleteAccount(id string) error { + return deleteAccount(database.DB, id) } -func DeleteAccountWithAudit(account model.Account, entityID string, auditLog model.AuditLog) error { +func DeleteAccountWithAudit(account model.Account, auditLog model.AuditLog) error { return database.DB.Transaction(func(tx *gorm.DB) error { - if err := deleteAccount(tx, account.ID, entityID); err != nil { + if err := deleteAccount(tx, account.ID); err != nil { return err } auditLog.AccountID = account.ID @@ -177,15 +175,11 @@ func DeleteAccountWithAudit(account model.Account, entityID string, auditLog mod }) } -func deleteAccount(db *gorm.DB, id string, entityID string) error { - now := time.Now() - result := db. - Model(&model.Account{}). - Where("id = ? AND deleted_at IS NULL", id). - Updates(map[string]interface{}{ - "deleted_at": &now, - "updated_by_entity_id": entityID, - }) +func deleteAccount(db *gorm.DB, id string) error { + if err := db.Where("account_id = ?", id).Delete(&model.Secret{}).Error; err != nil { + return err + } + result := db.Where("id = ?", id).Delete(&model.Account{}) if result.Error != nil { return result.Error } diff --git a/vault/service/app_secret.go b/vault/service/app_secret.go index e5d696d..1b55de6 100644 --- a/vault/service/app_secret.go +++ b/vault/service/app_secret.go @@ -5,7 +5,6 @@ import ( "fmt" "regexp" "strings" - "time" "github.com/gaucho-racing/ulid-go" "github.com/gaucho-racing/vault/vault/database" @@ -39,7 +38,6 @@ var appSecretIdentifierPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9_-]*$`) func GetAllApplications() ([]ApplicationWithSecretCount, error) { applications := []model.Application{} if err := database.DB. - Where("deleted_at IS NULL"). Order("name ASC"). Find(&applications).Error; err != nil { return []ApplicationWithSecretCount{}, err @@ -67,7 +65,7 @@ func GetAllApplications() ([]ApplicationWithSecretCount, error) { func GetApplicationByID(id string) (model.Application, error) { var application model.Application - if err := database.DB.Where("id = ? AND deleted_at IS NULL", id).First(&application).Error; err != nil { + if err := database.DB.Where("id = ?", id).First(&application).Error; err != nil { return model.Application{}, err } return application, nil @@ -108,26 +106,12 @@ func UpdateApplication(application model.Application) (model.Application, error) return application, nil } -func DeleteApplication(application model.Application, entityID string) error { - now := time.Now() +func DeleteApplication(application model.Application) error { return database.DB.Transaction(func(tx *gorm.DB) error { - if err := tx. - Model(&model.AppSecret{}). - Where("application_id = ? AND deleted_at IS NULL", application.ID). - Updates(map[string]interface{}{ - "deleted_at": &now, - "updated_by_entity_id": entityID, - }).Error; err != nil { + if err := tx.Where("application_id = ?", application.ID).Delete(&model.AppSecret{}).Error; err != nil { return err } - - result := tx. - Model(&model.Application{}). - Where("id = ? AND deleted_at IS NULL", application.ID). - Updates(map[string]interface{}{ - "deleted_at": &now, - "updated_by_entity_id": entityID, - }) + result := tx.Where("id = ?", application.ID).Delete(&model.Application{}) if result.Error != nil { return result.Error } @@ -141,7 +125,7 @@ func DeleteApplication(application model.Application, entityID string) error { func GetAppSecretsForApplication(applicationID string) ([]model.AppSecret, error) { secrets := []model.AppSecret{} if err := database.DB. - Where("application_id = ? AND deleted_at IS NULL", applicationID). + Where("application_id = ?", applicationID). Order("key ASC"). Find(&secrets).Error; err != nil { return []model.AppSecret{}, err @@ -152,7 +136,7 @@ func GetAppSecretsForApplication(applicationID string) ([]model.AppSecret, error func GetAppSecretForApplication(applicationID string, secretID string) (model.AppSecret, error) { var secret model.AppSecret if err := database.DB. - Where("id = ? AND application_id = ? AND deleted_at IS NULL", secretID, applicationID). + Where("id = ? AND application_id = ?", secretID, applicationID). First(&secret).Error; err != nil { return model.AppSecret{}, err } @@ -188,15 +172,10 @@ func UpdateAppSecret(secret model.AppSecret) (model.AppSecret, error) { return secret, nil } -func DeleteAppSecret(applicationID string, secretID string, entityID string) error { - now := time.Now() +func DeleteAppSecret(applicationID string, secretID string) error { result := database.DB. - Model(&model.AppSecret{}). - Where("id = ? AND application_id = ? AND deleted_at IS NULL", secretID, applicationID). - Updates(map[string]interface{}{ - "deleted_at": &now, - "updated_by_entity_id": entityID, - }) + Where("id = ? AND application_id = ?", secretID, applicationID). + Delete(&model.AppSecret{}) if result.Error != nil { return result.Error } @@ -240,7 +219,7 @@ func getAppSecretCountsByApplicationID(applicationIDs []string) (map[string]int6 if err := database.DB. Model(&model.AppSecret{}). Select("application_id, count(*) as secret_count"). - Where("application_id IN ? AND deleted_at IS NULL", applicationIDs). + Where("application_id IN ?", applicationIDs). Group("application_id"). Scan(&counts).Error; err != nil { return map[string]int64{}, err diff --git a/vault/service/secret.go b/vault/service/secret.go index 6f10bb4..812fc44 100644 --- a/vault/service/secret.go +++ b/vault/service/secret.go @@ -3,7 +3,6 @@ package service import ( "errors" "strings" - "time" "github.com/gaucho-racing/ulid-go" "github.com/gaucho-racing/vault/vault/database" @@ -16,7 +15,7 @@ var ErrSecretKeyRequired = errors.New("secret key is required") func GetSecretsForAccount(accountID string) ([]model.Secret, error) { secrets := []model.Secret{} if err := database.DB. - Where("account_id = ? AND deleted_at IS NULL", accountID). + Where("account_id = ?", accountID). Order("key ASC"). Find(&secrets).Error; err != nil { return []model.Secret{}, err @@ -26,7 +25,7 @@ func GetSecretsForAccount(accountID string) ([]model.Secret, error) { func GetSecretByID(id string) (model.Secret, error) { var secret model.Secret - if err := database.DB.Where("id = ? AND deleted_at IS NULL", id).First(&secret).Error; err != nil { + if err := database.DB.Where("id = ?", id).First(&secret).Error; err != nil { return model.Secret{}, err } return secret, nil @@ -34,7 +33,7 @@ func GetSecretByID(id string) (model.Secret, error) { func GetSecretForAccount(accountID string, secretID string) (model.Secret, error) { var secret model.Secret - if err := database.DB.Where("id = ? AND account_id = ? AND deleted_at IS NULL", secretID, accountID).First(&secret).Error; err != nil { + if err := database.DB.Where("id = ? AND account_id = ?", secretID, accountID).First(&secret).Error; err != nil { return model.Secret{}, err } return secret, nil @@ -69,15 +68,10 @@ func UpdateSecret(secret model.Secret) (model.Secret, error) { return secret, nil } -func DeleteSecret(accountID string, secretID string, entityID string) error { - now := time.Now() +func DeleteSecret(accountID string, secretID string) error { result := database.DB. - Model(&model.Secret{}). Where("id = ? AND account_id = ?", secretID, accountID). - Updates(map[string]interface{}{ - "deleted_at": &now, - "updated_by_entity_id": entityID, - }) + Delete(&model.Secret{}) if result.Error != nil { return result.Error }