fix: harden license lifecycle (#547)

This commit is contained in:
sagit
2026-08-13 09:25:05 +08:00
committed by GitHub
parent c56798e991
commit 5041c722c9
17 changed files with 734 additions and 85 deletions
@@ -15,14 +15,27 @@ var publicConfigKeys = map[string]struct{}{
"app_favicon": {},
"app_bg_image": {},
"cloudflare_site_key": {},
"is_commercial": {},
"hide_footer_brand": {},
}
var sensitiveConfigKeys = map[string]struct{}{
"jwt_secret": {},
"license_key": {},
"license_expiry": {},
"license_machine_id": {},
"machine_fingerprint": {},
"cloudflare_secret_key": {},
}
var systemManagedConfigKeys = map[string]struct{}{
"license_key": {},
"license_expiry": {},
"license_machine_id": {},
"is_commercial": {},
"machine_fingerprint": {},
}
func PolicyForConfig(name string) ConfigAccessPolicy {
if IsPublicConfigKey(name) {
return ConfigAccessPublic
@@ -43,6 +56,11 @@ func IsSensitiveConfigKey(name string) bool {
return ok
}
func IsSystemManagedConfigKey(name string) bool {
_, ok := systemManagedConfigKeys[normalizeConfigKey(name)]
return ok
}
func FilterSensitiveConfigs(in map[string]string) map[string]string {
if len(in) == 0 {
return map[string]string{}
@@ -57,6 +75,20 @@ func FilterSensitiveConfigs(in map[string]string) map[string]string {
return out
}
func FilterBackupConfigs(in map[string]string) map[string]string {
if len(in) == 0 {
return map[string]string{}
}
out := make(map[string]string, len(in))
for name, value := range in {
if IsSensitiveConfigKey(name) || IsSystemManagedConfigKey(name) {
continue
}
out[name] = value
}
return out
}
func normalizeConfigKey(name string) string {
return strings.ToLower(strings.TrimSpace(name))
}
@@ -13,6 +13,8 @@ func TestConfigPolicy(t *testing.T) {
{name: "app_favicon is public", key: "app_favicon", want: ConfigAccessPublic},
{name: "app_bg_image is public", key: "app_bg_image", want: ConfigAccessPublic},
{name: "cloudflare_site_key is public", key: "cloudflare_site_key", want: ConfigAccessPublic},
{name: "is_commercial is public", key: "is_commercial", want: ConfigAccessPublic},
{name: "hide_footer_brand is public", key: "hide_footer_brand", want: ConfigAccessPublic},
{name: "jwt_secret is sensitive", key: "jwt_secret", want: ConfigAccessSensitive},
{name: "license_key is sensitive", key: "license_key", want: ConfigAccessSensitive},
{name: "cloudflare_secret_key is sensitive", key: "cloudflare_secret_key", want: ConfigAccessSensitive},
@@ -29,14 +31,14 @@ func TestConfigPolicy(t *testing.T) {
}
func TestConfigPolicyHelpers(t *testing.T) {
publicKeys := []string{"app_name", "app_logo", "app_favicon", "app_bg_image", "cloudflare_site_key"}
publicKeys := []string{"app_name", "app_logo", "app_favicon", "app_bg_image", "cloudflare_site_key", "is_commercial", "hide_footer_brand"}
for _, key := range publicKeys {
if !IsPublicConfigKey(key) {
t.Fatalf("expected %q to be public", key)
}
}
sensitiveKeys := []string{"jwt_secret", "license_key", "cloudflare_secret_key"}
sensitiveKeys := []string{"jwt_secret", "license_key", "license_expiry", "license_machine_id", "machine_fingerprint", "cloudflare_secret_key"}
for _, key := range sensitiveKeys {
if !IsSensitiveConfigKey(key) {
t.Fatalf("expected %q to be sensitive", key)
@@ -67,3 +69,30 @@ func TestConfigPolicyHelpers(t *testing.T) {
t.Fatal("expected cloudflare_secret_key to be filtered out")
}
}
func TestFilterBackupConfigsOmitsLicenseState(t *testing.T) {
filtered := FilterBackupConfigs(map[string]string{
"app_name": "Brand",
"license_key": "secret-license",
"license_expiry": "never",
"license_machine_id": "machine-id",
"is_commercial": "true",
"machine_fingerprint": "fingerprint",
})
if len(filtered) != 1 || filtered["app_name"] != "Brand" {
t.Fatalf("unexpected backup configs: %+v", filtered)
}
}
func TestSystemManagedConfigKeys(t *testing.T) {
for _, key := range []string{"license_key", "license_expiry", "license_machine_id", "is_commercial", "machine_fingerprint"} {
if !IsSystemManagedConfigKey(key) {
t.Fatalf("expected %s to be system managed", key)
}
}
for _, key := range []string{"jwt_secret", "cloudflare_secret_key", "app_name"} {
if IsSystemManagedConfigKey(key) {
t.Fatalf("did not expect %s to be system managed", key)
}
}
}
+28 -3
View File
@@ -473,6 +473,14 @@ func seedData(db *gorm.DB) {
appNameConfig := model.ViteConfig{ID: 1, Name: "app_name", Value: "flux", Time: 1755147963000}
db.Where("id = ?", 1).FirstOrCreate(&appNameConfig)
now := time.Now().UnixMilli()
for name, value := range map[string]string{
"is_commercial": "false",
"hide_footer_brand": "false",
} {
cfg := model.ViteConfig{Name: name, Value: value, Time: now}
db.Where("name = ?", name).FirstOrCreate(&cfg)
}
}
// ─── User Queries ────────────────────────────────────────────────────
@@ -600,6 +608,23 @@ func (r *Repository) UpsertConfig(name, value string, now int64) error {
}).Create(&model.ViteConfig{Name: name, Value: value, Time: now}).Error
}
func (r *Repository) UpsertConfigs(values map[string]string, now int64) error {
if r == nil || r.db == nil {
return errors.New("repository not initialized")
}
return r.db.Transaction(func(tx *gorm.DB) error {
for name, value := range values {
if err := tx.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "name"}},
DoUpdates: clause.AssignmentColumns([]string{"value", "time"}),
}).Create(&model.ViteConfig{Name: name, Value: value, Time: now}).Error; err != nil {
return err
}
}
return nil
})
}
// ─── Announcement Queries ────────────────────────────────────────────
func (r *Repository) GetAnnouncement() (*model.Announcement, error) {
@@ -1967,7 +1992,7 @@ func (r *Repository) ExportAll() (*model.BackupData, error) {
if err != nil {
return nil, fmt.Errorf("export configs failed: %w", err)
}
backup.Configs = FilterSensitiveConfigs(configs)
backup.Configs = FilterBackupConfigs(configs)
return backup, nil
}
@@ -2047,7 +2072,7 @@ func (r *Repository) ExportPartial(types []string) (*model.BackupData, error) {
if err != nil {
return nil, fmt.Errorf("export configs failed: %w", err)
}
backup.Configs = FilterSensitiveConfigs(v)
backup.Configs = FilterBackupConfigs(v)
}
return backup, nil
}
@@ -2839,7 +2864,7 @@ func importPermissions(tx *gorm.DB, permissions []model.PermissionBackup, _ int6
}
func importConfigs(tx *gorm.DB, configs map[string]string, now int64) (int, error) {
configs = FilterSensitiveConfigs(configs)
configs = FilterBackupConfigs(configs)
count := 0
for name, value := range configs {
err := tx.Clauses(clause.OnConflict{
@@ -97,6 +97,10 @@ func TestExportAllOmitsSensitiveConfigs(t *testing.T) {
seedConfig(t, r, "cloudflare_site_key", "site-key")
seedConfig(t, r, "jwt_secret", "jwt-secret")
seedConfig(t, r, "license_key", "license-secret")
seedConfig(t, r, "license_expiry", "2030-01-02T00:00:00.000Z")
seedConfig(t, r, "license_machine_id", "machine-id")
seedConfig(t, r, "is_commercial", "true")
seedConfig(t, r, "machine_fingerprint", "machine-fingerprint")
seedConfig(t, r, "cloudflare_secret_key", "cloudflare-secret")
for _, tc := range []struct {
@@ -117,7 +121,7 @@ func TestExportAllOmitsSensitiveConfigs(t *testing.T) {
if backup.Configs["cloudflare_site_key"] != "site-key" {
t.Fatalf("expected public config in export, got %+v", backup.Configs)
}
for _, key := range []string{"jwt_secret", "license_key", "cloudflare_secret_key"} {
for _, key := range []string{"jwt_secret", "license_key", "license_expiry", "license_machine_id", "is_commercial", "machine_fingerprint", "cloudflare_secret_key"} {
if _, ok := backup.Configs[key]; ok {
t.Fatalf("expected %s to be omitted from export, got %+v", key, backup.Configs)
}
@@ -136,12 +140,20 @@ func TestImportIgnoresSensitiveConfigs(t *testing.T) {
seedConfig(t, r, "app_name", "before")
seedConfig(t, r, "jwt_secret", "jwt-before")
seedConfig(t, r, "license_key", "license-before")
seedConfig(t, r, "license_expiry", "expiry-before")
seedConfig(t, r, "license_machine_id", "machine-before")
seedConfig(t, r, "is_commercial", "true")
seedConfig(t, r, "machine_fingerprint", "fingerprint-before")
seedConfig(t, r, "cloudflare_secret_key", "cloudflare-before")
backup := &model.BackupData{Configs: map[string]string{
"app_name": "after",
"jwt_secret": "jwt-after",
"license_key": "license-after",
"license_expiry": "expiry-after",
"license_machine_id": "machine-after",
"is_commercial": "false",
"machine_fingerprint": "fingerprint-after",
"cloudflare_secret_key": "cloudflare-after",
}}
@@ -156,6 +168,10 @@ func TestImportIgnoresSensitiveConfigs(t *testing.T) {
assertConfigValue(t, r, "app_name", "after")
assertConfigValue(t, r, "jwt_secret", "jwt-before")
assertConfigValue(t, r, "license_key", "license-before")
assertConfigValue(t, r, "license_expiry", "expiry-before")
assertConfigValue(t, r, "license_machine_id", "machine-before")
assertConfigValue(t, r, "is_commercial", "true")
assertConfigValue(t, r, "machine_fingerprint", "fingerprint-before")
assertConfigValue(t, r, "cloudflare_secret_key", "cloudflare-before")
}