From fac6cfb931c1204434ce20a7142645690252e21d Mon Sep 17 00:00:00 2001 From: DioCrafts Date: Sat, 21 Mar 2026 23:21:49 +0100 Subject: [PATCH 1/2] fix(security): auto-generate JWT and encryption secrets on first boot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New installations no longer ship with hardcoded secrets. On first boot, cryptographically secure random values are generated and persisted in a new system_secrets table. Subsequent restarts read from the database, ensuring stable secrets without any manual configuration. Priority chain: env var > database > auto-generate. Upgrade safety for existing installations: - If KITE_ENCRYPT_KEY env var was set: unchanged, value is persisted to DB - If running with the hardcoded default: existing encrypted data is detected, the default is preserved in DB, and a loud warning is emitted urging operators to rotate to a secure key - JWT secret follows the same logic (session invalidation on rotation is acceptable — users simply re-authenticate) Helm chart changes: - jwtSecret and encryptKey default to empty (auto-generated by the app) - Secret template only injects env vars when values are non-empty - existingSecret workflow is unaffected Removes the redundant KITE_ENCRYPT_KEY warning from LoadEnvs (now handled by EnsureSecrets with better migration-aware logic). --- charts/kite/templates/secret.yaml | 4 + charts/kite/values.yaml | 10 +-- docs/config/chart-values.md | 4 +- docs/zh/config/chart-values.md | 4 +- main.go | 1 + pkg/common/common.go | 2 - pkg/model/model.go | 1 + pkg/model/system_secret.go | 140 ++++++++++++++++++++++++++++++ 8 files changed, 154 insertions(+), 12 deletions(-) create mode 100644 pkg/model/system_secret.go diff --git a/charts/kite/templates/secret.yaml b/charts/kite/templates/secret.yaml index 1f474ec2..c1bcae96 100644 --- a/charts/kite/templates/secret.yaml +++ b/charts/kite/templates/secret.yaml @@ -8,8 +8,12 @@ metadata: {{- include "kite.labels" . | nindent 4 }} type: Opaque data: + {{- if .Values.jwtSecret }} JWT_SECRET: {{ .Values.jwtSecret | b64enc | quote }} + {{- end }} + {{- if .Values.encryptKey }} KITE_ENCRYPT_KEY: {{ .Values.encryptKey | b64enc | quote }} + {{- end }} {{- if ne .Values.db.type "sqlite" }} DB_TYPE: {{ .Values.db.type | b64enc | quote }} DB_DSN: {{ .Values.db.dsn | b64enc | quote }} diff --git a/charts/kite/values.yaml b/charts/kite/values.yaml index 117b693c..78b3be83 100644 --- a/charts/kite/values.yaml +++ b/charts/kite/values.yaml @@ -49,15 +49,13 @@ basePath: "" # Be careful with this setting in production anonymousUserEnabled: false -# This is the key used for signing JWT tokens -# Change this in production +# Secret key for signing JWT tokens. Auto-generated if empty. # Ignored if using existingSecret -jwtSecret: "kite-default-jwt-secret-key-change-in-production" +jwtSecret: "" -# This is the key used for encrypting sensitive data -# Change this in production +# Key for encrypting sensitive data. Auto-generated if empty. # Ignored if using existingSecret -encryptKey: "kite-default-encryption-key-change-in-production" +encryptKey: "" # Superuser configuration # Used to create an initial superuser account on first startup diff --git a/docs/config/chart-values.md b/docs/config/chart-values.md index 7a494a3e..59cdd6a7 100644 --- a/docs/config/chart-values.md +++ b/docs/config/chart-values.md @@ -21,8 +21,8 @@ This document describes all available configuration options for the Kite Helm Ch | Parameter | Description | Default | | ---------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------- | | `anonymousUserEnabled` | Enable anonymous user access with full admin privileges. Use with caution in production. | `false` | -| `jwtSecret` | Secret key used for signing JWT tokens. Change this in production. | `"kite-default-jwt-secret-key-change-in-production"` | -| `encryptKey` | Secret key used for encrypting sensitive data. Change this in production. | `"kite-default-encryption-key-change-in-production"` | +| `jwtSecret` | Secret key for signing JWT tokens. Auto-generated on first boot if empty. | `""` | +| `encryptKey` | Secret key for encrypting sensitive data. Auto-generated on first boot if empty. | `""` | | `host` | Hostname for the application | `""` | ## Database Configuration diff --git a/docs/zh/config/chart-values.md b/docs/zh/config/chart-values.md index 402aa1b4..fc03377a 100644 --- a/docs/zh/config/chart-values.md +++ b/docs/zh/config/chart-values.md @@ -21,8 +21,8 @@ | 参数 | 描述 | 默认值 | | ---------------------- | ---------------------------------------------------------- | ---------------------------------------------------- | | `anonymousUserEnabled` | 启用匿名用户访问,拥有完全管理员权限。生产环境请谨慎使用。 | `false` | -| `jwtSecret` | 用于签名 JWT 令牌的密钥。生产环境请修改此值。 | `"kite-default-jwt-secret-key-change-in-production"` | -| `encryptKey` | 用于加密敏感数据的密钥。生产环境请修改此值。 | `"kite-default-encryption-key-change-in-production"` | +| `jwtSecret` | 用于签名 JWT 令牌的密钥。为空时首次启动自动生成。 | `""` | +| `encryptKey` | 用于加密敏感数据的密钥。为空时首次启动自动生成。 | `""` | | `host` | 应用程序的主机名 | `""` | ## 数据库配置 diff --git a/main.go b/main.go index 6d8a74f0..4a3c8b15 100644 --- a/main.go +++ b/main.go @@ -242,6 +242,7 @@ func main() { r.Use(middleware.Logger()) r.Use(middleware.CORS()) model.InitDB() + model.EnsureSecrets() if _, err := model.GetGeneralSetting(); err != nil { klog.Warningf("Failed to load general setting: %v", err) } diff --git a/pkg/common/common.go b/pkg/common/common.go index 6eaf478f..baa818eb 100644 --- a/pkg/common/common.go +++ b/pkg/common/common.go @@ -85,8 +85,6 @@ func LoadEnvs() { if key := os.Getenv("KITE_ENCRYPT_KEY"); key != "" { KiteEncryptKey = key - } else { - klog.Warningf("KITE_ENCRYPT_KEY is not set, using default key, this is not secure for production!") } if v := os.Getenv("ANONYMOUS_USER_ENABLED"); v == "true" { diff --git a/pkg/model/model.go b/pkg/model/model.go index d4c9551f..7d3e6bb6 100644 --- a/pkg/model/model.go +++ b/pkg/model/model.go @@ -91,6 +91,7 @@ func InitDB() { } } models := []interface{}{ + SystemSecret{}, User{}, Cluster{}, GeneralSetting{}, diff --git a/pkg/model/system_secret.go b/pkg/model/system_secret.go new file mode 100644 index 00000000..97424b6e --- /dev/null +++ b/pkg/model/system_secret.go @@ -0,0 +1,140 @@ +package model + +import ( + "crypto/rand" + "encoding/base64" + "errors" + "os" + + "github.com/zxh326/kite/pkg/common" + "gorm.io/gorm" + "k8s.io/klog/v2" +) + +// SystemSecret stores auto-generated application secrets in the database. +// Values are plain text (not SecretString) to avoid circular encryption. +type SystemSecret struct { + Name string `json:"name" gorm:"primaryKey;column:name;type:varchar(64)"` + Value string `json:"value" gorm:"column:value;type:text;not null"` +} + +const ( + secretNameJWT = "jwt_secret" + secretNameEncrypt = "encrypt_key" + + defaultJWTSecret = "kite-default-jwt-secret-key-change-in-production" + defaultEncryptKey = "kite-default-encryption-key-change-in-production" +) + +// EnsureSecrets guarantees that JwtSecret and KiteEncryptKey hold +// cryptographically secure values. Must be called after InitDB() +// and before any code that reads SecretString columns. +// +// Priority: env var > DB stored value > auto-generated. +func EnsureSecrets() { + common.JwtSecret = ensureOneSecret( + secretNameJWT, common.JwtSecret, "JWT_SECRET", defaultJWTSecret, false, + os.Getenv("JWT_SECRET") != "", + ) + common.KiteEncryptKey = ensureOneSecret( + secretNameEncrypt, common.KiteEncryptKey, "KITE_ENCRYPT_KEY", defaultEncryptKey, true, + os.Getenv("KITE_ENCRYPT_KEY") != "", + ) +} + +func ensureOneSecret(dbName, currentValue, envName, knownDefault string, isEncryptionKey, envWasSet bool) string { + if envWasSet { + return currentValue + } + + stored, dbErr := loadSecret(dbName) + if dbErr != nil { + klog.Fatalf("Cannot read %s from database: %v (refusing to proceed with ambiguous secret state)", envName, dbErr) + } + if stored != "" { + return stored + } + + if isEncryptionKey && hasExistingEncryptedData() { + effective := persistSecret(dbName, currentValue) + klog.Warningf("════════════════════════════════════════════════════════════") + klog.Warningf(" %s is using the insecure hardcoded default.", envName) + klog.Warningf(" Existing encrypted data has been preserved.") + klog.Warningf(" Please set %s to a secure random value", envName) + klog.Warningf(" and re-encrypt your data.") + klog.Warningf("════════════════════════════════════════════════════════════") + return effective + } + + secret := persistSecret(dbName, generateRandomSecret(32)) + klog.Infof("Auto-generated %s and stored in database (first boot)", envName) + return secret +} + +func generateRandomSecret(n int) string { + b := make([]byte, n) + if _, err := rand.Read(b); err != nil { + klog.Fatalf("Failed to generate random secret: %v", err) + } + return base64.RawURLEncoding.EncodeToString(b) +} + +func loadSecret(name string) (string, error) { + var s SystemSecret + err := DB.Where("name = ?", name).First(&s).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return "", nil + } + if err != nil { + return "", err + } + return s.Value, nil +} + +// persistSecret inserts the secret if no row exists yet. If a row already +// exists the stored value is returned (first writer wins). Fatals on +// unrecoverable DB errors. +func persistSecret(name, value string) string { + var existing SystemSecret + err := DB.Where("name = ?", name).First(&existing).Error + + if errors.Is(err, gorm.ErrRecordNotFound) { + if err := DB.Create(&SystemSecret{Name: name, Value: value}).Error; err != nil { + if stored, readErr := loadSecret(name); readErr == nil && stored != "" { + klog.Infof("Secret %q was created by another instance, adopting its value", name) + return stored + } + klog.Fatalf("Failed to persist secret %q and no stored winner found: %v", name, err) + } + return value + } + if err != nil { + klog.Fatalf("Failed to read secret %q from database: %v", name, err) + } + return existing.Value +} + +// hasExistingEncryptedData returns true when the database contains rows with +// non-empty SecretString columns. Returns true on query errors (fail-safe). +func hasExistingEncryptedData() bool { + checks := []struct { + model interface{} + where string + }{ + {&Cluster{}, "config IS NOT NULL AND config != ''"}, + {&OAuthProvider{}, "client_secret IS NOT NULL AND client_secret != ''"}, + {&User{}, "api_key IS NOT NULL AND api_key != ''"}, + {&GeneralSetting{}, "ai_api_key IS NOT NULL AND ai_api_key != ''"}, + } + for _, c := range checks { + var count int64 + if err := DB.Model(c.model).Where(c.where).Count(&count).Error; err != nil { + klog.Warningf("Failed to check for encrypted data (%T): %v — assuming data exists (fail-safe)", c.model, err) + return true + } + if count > 0 { + return true + } + } + return false +} From c3a04bc9e5258e388f1d318f76281895e83e9be2 Mon Sep 17 00:00:00 2001 From: Zzde Date: Thu, 26 Mar 2026 18:21:10 +0800 Subject: [PATCH 2/2] review Signed-off-by: Zzde --- charts/kite/values.yaml | 5 +- docs/config/chart-values.md | 2 +- docs/zh/config/chart-values.md | 2 +- main.go | 1 - pkg/common/common.go | 5 +- pkg/model/general_setting.go | 54 ++++++++++++- pkg/model/model.go | 1 - pkg/model/system_secret.go | 140 --------------------------------- 8 files changed, 62 insertions(+), 148 deletions(-) delete mode 100644 pkg/model/system_secret.go diff --git a/charts/kite/values.yaml b/charts/kite/values.yaml index 78b3be83..904928b2 100644 --- a/charts/kite/values.yaml +++ b/charts/kite/values.yaml @@ -53,9 +53,10 @@ anonymousUserEnabled: false # Ignored if using existingSecret jwtSecret: "" -# Key for encrypting sensitive data. Auto-generated if empty. +# This is the key used for encrypting sensitive data +# Change this in production # Ignored if using existingSecret -encryptKey: "" +encryptKey: "kite-default-encryption-key-change-in-production" # Superuser configuration # Used to create an initial superuser account on first startup diff --git a/docs/config/chart-values.md b/docs/config/chart-values.md index 59cdd6a7..f6c921b8 100644 --- a/docs/config/chart-values.md +++ b/docs/config/chart-values.md @@ -22,7 +22,7 @@ This document describes all available configuration options for the Kite Helm Ch | ---------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------- | | `anonymousUserEnabled` | Enable anonymous user access with full admin privileges. Use with caution in production. | `false` | | `jwtSecret` | Secret key for signing JWT tokens. Auto-generated on first boot if empty. | `""` | -| `encryptKey` | Secret key for encrypting sensitive data. Auto-generated on first boot if empty. | `""` | +| `encryptKey` | Secret key used for encrypting sensitive data. Change this in production. | `"kite-default-encryption-key-change-in-production"` | | `host` | Hostname for the application | `""` | ## Database Configuration diff --git a/docs/zh/config/chart-values.md b/docs/zh/config/chart-values.md index fc03377a..03a1931e 100644 --- a/docs/zh/config/chart-values.md +++ b/docs/zh/config/chart-values.md @@ -22,7 +22,7 @@ | ---------------------- | ---------------------------------------------------------- | ---------------------------------------------------- | | `anonymousUserEnabled` | 启用匿名用户访问,拥有完全管理员权限。生产环境请谨慎使用。 | `false` | | `jwtSecret` | 用于签名 JWT 令牌的密钥。为空时首次启动自动生成。 | `""` | -| `encryptKey` | 用于加密敏感数据的密钥。为空时首次启动自动生成。 | `""` | +| `encryptKey` | 用于加密敏感数据的密钥。生产环境请修改此值。 | `"kite-default-encryption-key-change-in-production"` | | `host` | 应用程序的主机名 | `""` | ## 数据库配置 diff --git a/main.go b/main.go index 4a3c8b15..6d8a74f0 100644 --- a/main.go +++ b/main.go @@ -242,7 +242,6 @@ func main() { r.Use(middleware.Logger()) r.Use(middleware.CORS()) model.InitDB() - model.EnsureSecrets() if _, err := model.GetGeneralSetting(); err != nil { klog.Warningf("Failed to load general setting: %v", err) } diff --git a/pkg/common/common.go b/pkg/common/common.go index baa818eb..b59b76ca 100644 --- a/pkg/common/common.go +++ b/pkg/common/common.go @@ -10,6 +10,7 @@ import ( const ( JWTExpirationSeconds = 24 * 60 * 60 // 24 hours + DefaultJWTSecret = "kite-default-jwt-secret-key-change-in-production" NodeTerminalPodName = "kite-node-terminal-agent" KubectlTerminalPodName = "kite-kubectl-agent" @@ -24,7 +25,7 @@ const ( var ( Port = "8080" - JwtSecret = "kite-default-jwt-secret-key-change-in-production" + JwtSecret = DefaultJWTSecret EnableAnalytics = false Host = "" Base = "" @@ -85,6 +86,8 @@ func LoadEnvs() { if key := os.Getenv("KITE_ENCRYPT_KEY"); key != "" { KiteEncryptKey = key + } else { + klog.Warningf("KITE_ENCRYPT_KEY is not set, using default key, this is not secure for production!") } if v := os.Getenv("ANONYMOUS_USER_ENABLED"); v == "true" { diff --git a/pkg/model/general_setting.go b/pkg/model/general_setting.go index 36ede60b..433d581a 100644 --- a/pkg/model/general_setting.go +++ b/pkg/model/general_setting.go @@ -1,11 +1,14 @@ package model import ( + "crypto/rand" + "encoding/base64" "errors" "strings" "github.com/zxh326/kite/pkg/common" "gorm.io/gorm" + "k8s.io/klog/v2" ) const DefaultGeneralAIModel = "gpt-4o-mini" @@ -38,6 +41,7 @@ type GeneralSetting struct { NodeTerminalImage string `json:"nodeTerminalImage" gorm:"column:node_terminal_image;type:varchar(255);not null;default:'busybox:latest'"` EnableAnalytics bool `json:"enableAnalytics" gorm:"column:enable_analytics;type:boolean;not null;default:true"` EnableVersionCheck bool `json:"enableVersionCheck" gorm:"column:enable_version_check;type:boolean;not null;default:true"` + JWTSecret SecretString `json:"-" gorm:"column:jwt_secret;type:text"` } func NormalizeGeneralAIProvider(provider string) string { @@ -91,8 +95,13 @@ func GetGeneralSetting() (*GeneralSetting, error) { setting.NodeTerminalImage = defaultNodeTerminalImage updates["node_terminal_image"] = defaultNodeTerminalImage } + if err := ensureJWTSecret(&setting, updates); err != nil { + return nil, err + } if len(updates) > 0 { - _ = DB.Model(&setting).Updates(updates).Error + if err := DB.Model(&setting).Updates(updates).Error; err != nil { + return nil, err + } } applyRuntimeGeneralSetting(&setting) return &setting, nil @@ -113,6 +122,9 @@ func GetGeneralSetting() (*GeneralSetting, error) { EnableAnalytics: common.EnableAnalytics, EnableVersionCheck: common.EnableVersionCheck, } + if err := ensureJWTSecret(&setting, nil); err != nil { + return nil, err + } if err := DB.Create(&setting).Error; err != nil { return nil, err } @@ -142,3 +154,43 @@ func applyRuntimeGeneralSetting(setting *GeneralSetting) { common.EnableAnalytics = setting.EnableAnalytics common.EnableVersionCheck = setting.EnableVersionCheck } + +func ensureJWTSecret(setting *GeneralSetting, updates map[string]interface{}) error { + storedSecret := strings.TrimSpace(string(setting.JWTSecret)) + configuredSecret := strings.TrimSpace(common.JwtSecret) + + switch { + case configuredSecret != "" && configuredSecret != common.DefaultJWTSecret: + if storedSecret != configuredSecret { + setting.JWTSecret = SecretString(configuredSecret) + if updates != nil { + updates["jwt_secret"] = setting.JWTSecret + } + } + common.JwtSecret = configuredSecret + return nil + case storedSecret != "" && storedSecret != common.DefaultJWTSecret: + common.JwtSecret = storedSecret + return nil + default: + generatedSecret, err := generateJWTSecret() + if err != nil { + return err + } + setting.JWTSecret = SecretString(generatedSecret) + common.JwtSecret = generatedSecret + if updates != nil { + updates["jwt_secret"] = setting.JWTSecret + } + klog.Warningf("JWT secret is using the insecure default value, generated a random secret and stored it in general setting") + return nil + } +} + +func generateJWTSecret() (string, error) { + buf := make([]byte, 32) + if _, err := rand.Read(buf); err != nil { + return "", err + } + return base64.RawURLEncoding.EncodeToString(buf), nil +} diff --git a/pkg/model/model.go b/pkg/model/model.go index 7d3e6bb6..d4c9551f 100644 --- a/pkg/model/model.go +++ b/pkg/model/model.go @@ -91,7 +91,6 @@ func InitDB() { } } models := []interface{}{ - SystemSecret{}, User{}, Cluster{}, GeneralSetting{}, diff --git a/pkg/model/system_secret.go b/pkg/model/system_secret.go deleted file mode 100644 index 97424b6e..00000000 --- a/pkg/model/system_secret.go +++ /dev/null @@ -1,140 +0,0 @@ -package model - -import ( - "crypto/rand" - "encoding/base64" - "errors" - "os" - - "github.com/zxh326/kite/pkg/common" - "gorm.io/gorm" - "k8s.io/klog/v2" -) - -// SystemSecret stores auto-generated application secrets in the database. -// Values are plain text (not SecretString) to avoid circular encryption. -type SystemSecret struct { - Name string `json:"name" gorm:"primaryKey;column:name;type:varchar(64)"` - Value string `json:"value" gorm:"column:value;type:text;not null"` -} - -const ( - secretNameJWT = "jwt_secret" - secretNameEncrypt = "encrypt_key" - - defaultJWTSecret = "kite-default-jwt-secret-key-change-in-production" - defaultEncryptKey = "kite-default-encryption-key-change-in-production" -) - -// EnsureSecrets guarantees that JwtSecret and KiteEncryptKey hold -// cryptographically secure values. Must be called after InitDB() -// and before any code that reads SecretString columns. -// -// Priority: env var > DB stored value > auto-generated. -func EnsureSecrets() { - common.JwtSecret = ensureOneSecret( - secretNameJWT, common.JwtSecret, "JWT_SECRET", defaultJWTSecret, false, - os.Getenv("JWT_SECRET") != "", - ) - common.KiteEncryptKey = ensureOneSecret( - secretNameEncrypt, common.KiteEncryptKey, "KITE_ENCRYPT_KEY", defaultEncryptKey, true, - os.Getenv("KITE_ENCRYPT_KEY") != "", - ) -} - -func ensureOneSecret(dbName, currentValue, envName, knownDefault string, isEncryptionKey, envWasSet bool) string { - if envWasSet { - return currentValue - } - - stored, dbErr := loadSecret(dbName) - if dbErr != nil { - klog.Fatalf("Cannot read %s from database: %v (refusing to proceed with ambiguous secret state)", envName, dbErr) - } - if stored != "" { - return stored - } - - if isEncryptionKey && hasExistingEncryptedData() { - effective := persistSecret(dbName, currentValue) - klog.Warningf("════════════════════════════════════════════════════════════") - klog.Warningf(" %s is using the insecure hardcoded default.", envName) - klog.Warningf(" Existing encrypted data has been preserved.") - klog.Warningf(" Please set %s to a secure random value", envName) - klog.Warningf(" and re-encrypt your data.") - klog.Warningf("════════════════════════════════════════════════════════════") - return effective - } - - secret := persistSecret(dbName, generateRandomSecret(32)) - klog.Infof("Auto-generated %s and stored in database (first boot)", envName) - return secret -} - -func generateRandomSecret(n int) string { - b := make([]byte, n) - if _, err := rand.Read(b); err != nil { - klog.Fatalf("Failed to generate random secret: %v", err) - } - return base64.RawURLEncoding.EncodeToString(b) -} - -func loadSecret(name string) (string, error) { - var s SystemSecret - err := DB.Where("name = ?", name).First(&s).Error - if errors.Is(err, gorm.ErrRecordNotFound) { - return "", nil - } - if err != nil { - return "", err - } - return s.Value, nil -} - -// persistSecret inserts the secret if no row exists yet. If a row already -// exists the stored value is returned (first writer wins). Fatals on -// unrecoverable DB errors. -func persistSecret(name, value string) string { - var existing SystemSecret - err := DB.Where("name = ?", name).First(&existing).Error - - if errors.Is(err, gorm.ErrRecordNotFound) { - if err := DB.Create(&SystemSecret{Name: name, Value: value}).Error; err != nil { - if stored, readErr := loadSecret(name); readErr == nil && stored != "" { - klog.Infof("Secret %q was created by another instance, adopting its value", name) - return stored - } - klog.Fatalf("Failed to persist secret %q and no stored winner found: %v", name, err) - } - return value - } - if err != nil { - klog.Fatalf("Failed to read secret %q from database: %v", name, err) - } - return existing.Value -} - -// hasExistingEncryptedData returns true when the database contains rows with -// non-empty SecretString columns. Returns true on query errors (fail-safe). -func hasExistingEncryptedData() bool { - checks := []struct { - model interface{} - where string - }{ - {&Cluster{}, "config IS NOT NULL AND config != ''"}, - {&OAuthProvider{}, "client_secret IS NOT NULL AND client_secret != ''"}, - {&User{}, "api_key IS NOT NULL AND api_key != ''"}, - {&GeneralSetting{}, "ai_api_key IS NOT NULL AND ai_api_key != ''"}, - } - for _, c := range checks { - var count int64 - if err := DB.Model(c.model).Where(c.where).Count(&count).Error; err != nil { - klog.Warningf("Failed to check for encrypted data (%T): %v — assuming data exists (fail-safe)", c.model, err) - return true - } - if count > 0 { - return true - } - } - return false -}