diff --git a/go.mod b/go.mod index b0f328ef5..e20b7e58b 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( github.com/getsentry/sentry-go v0.31.1 github.com/go-errors/errors v1.5.1 github.com/go-git/go-git/v5 v5.13.2 + github.com/go-playground/validator/v10 v10.24.0 github.com/go-xmlfmt/xmlfmt v1.1.3 github.com/golang-jwt/jwt/v5 v5.2.1 github.com/golangci/golangci-lint v1.63.4 @@ -132,6 +133,7 @@ require ( github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/fvbommel/sortorder v1.1.0 // indirect github.com/fzipp/gocyclo v0.6.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/getkin/kin-openapi v0.124.0 // indirect github.com/ghostiam/protogetter v0.3.8 // indirect github.com/go-critic/go-critic v0.11.5 // indirect @@ -141,6 +143,8 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-toolsmith/astcast v1.1.0 // indirect github.com/go-toolsmith/astcopy v1.1.0 // indirect github.com/go-toolsmith/astequal v1.2.0 // indirect @@ -206,6 +210,7 @@ require ( github.com/ldez/grignotin v0.7.0 // indirect github.com/ldez/tagliatelle v0.7.1 // indirect github.com/ldez/usetesting v0.4.2 // indirect + github.com/leodido/go-urn v1.4.0 // indirect github.com/leonklingele/grouper v1.1.2 // indirect github.com/lib/pq v1.10.9 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect @@ -327,9 +332,9 @@ require ( golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329 // indirect golang.org/x/exp/typeparams v0.0.0-20241108190413-2d47ceb2692f // indirect golang.org/x/net v0.34.0 // indirect - golang.org/x/sync v0.10.0 // indirect + golang.org/x/sync v0.11.0 // indirect golang.org/x/sys v0.30.0 // indirect - golang.org/x/text v0.21.0 // indirect + golang.org/x/text v0.22.0 // indirect golang.org/x/tools v0.28.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d // indirect diff --git a/go.sum b/go.sum index 4c73d9f25..152604dbb 100644 --- a/go.sum +++ b/go.sum @@ -285,6 +285,8 @@ github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQ github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo= github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA= +github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/getkin/kin-openapi v0.124.0 h1:VSFNMB9C9rTKBnQ/fpyDU8ytMTr4dWI9QovSKj9kz/M= github.com/getkin/kin-openapi v0.124.0/go.mod h1:wb1aSZA/iWmorQP9KTAS/phLj/t17B5jT7+fS8ed9NM= github.com/getsentry/sentry-go v0.31.1 h1:ELVc0h7gwyhnXHDouXkhqTFSO5oslsRDk0++eyE0KJ4= @@ -323,6 +325,14 @@ github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1 github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg= +github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus= github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= @@ -639,6 +649,8 @@ github.com/ldez/tagliatelle v0.7.1 h1:bTgKjjc2sQcsgPiT902+aadvMjCeMHrY7ly2XKFORI github.com/ldez/tagliatelle v0.7.1/go.mod h1:3zjxUpsNB2aEZScWiZTHrAXOl1x25t3cRmzfK1mlo2I= github.com/ldez/usetesting v0.4.2 h1:J2WwbrFGk3wx4cZwSMiCQQ00kjGR0+tuuyW0Lqm4lwA= github.com/ldez/usetesting v0.4.2/go.mod h1:eEs46T3PpQ+9RgN9VjpY6qWdiw2/QmfiDeWmdZdrjIQ= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leonklingele/grouper v1.1.2 h1:o1ARBDLOmmasUaNDesWqWCIFH3u7hoFlM84YrjT3mIY= github.com/leonklingele/grouper v1.1.2/go.mod h1:6D0M/HVkhs2yRKRFZUoGjeDy7EZTfFBE9gl4kjmIGkA= github.com/lib/pq v0.0.0-20150723085316-0dad96c0b94f/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= @@ -1209,8 +1221,8 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1313,8 +1325,8 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/internal/functions/serve/serve.go b/internal/functions/serve/serve.go index 2f09fed16..978cb9e70 100644 --- a/internal/functions/serve/serve.go +++ b/internal/functions/serve/serve.go @@ -218,7 +218,7 @@ func populatePerFunctionConfigs(cwd, importMapPath string, noVerifyJWT *bool, fs } binds := []string{} for slug, fc := range functionsConfig { - if !fc.IsEnabled() { + if !fc.Enabled { fmt.Fprintln(os.Stderr, "Skipped serving Function:", slug) continue } diff --git a/internal/start/start.go b/internal/start/start.go index 1fdaf1ebe..9e34b0709 100644 --- a/internal/start/start.go +++ b/internal/start/start.go @@ -502,7 +502,7 @@ EOF fmt.Sprintf("GOTRUE_MFA_MAX_ENROLLED_FACTORS=%v", utils.Config.Auth.MFA.MaxEnrolledFactors), } - if utils.Config.Auth.Email.Smtp != nil && utils.Config.Auth.Email.Smtp.IsEnabled() { + if utils.Config.Auth.Email.Smtp != nil && utils.Config.Auth.Email.Smtp.Enabled { env = append(env, fmt.Sprintf("GOTRUE_SMTP_HOST=%s", utils.Config.Auth.Email.Smtp.Host), fmt.Sprintf("GOTRUE_SMTP_PORT=%d", utils.Config.Auth.Email.Smtp.Port), @@ -984,7 +984,7 @@ EOF "SUPABASE_ANON_KEY=" + utils.Config.Auth.AnonKey, "SUPABASE_SERVICE_KEY=" + utils.Config.Auth.ServiceRoleKey, "LOGFLARE_API_KEY=" + utils.Config.Analytics.ApiKey, - "OPENAI_API_KEY=" + utils.Config.Studio.OpenaiApiKey, + "OPENAI_API_KEY=" + utils.Config.Studio.OpenaiApiKey.Value, fmt.Sprintf("LOGFLARE_URL=http://%v:4000", utils.LogflareId), fmt.Sprintf("NEXT_PUBLIC_ENABLE_LOGS=%v", utils.Config.Analytics.Enabled), fmt.Sprintf("NEXT_ANALYTICS_BACKEND_PROVIDER=%v", utils.Config.Analytics.Backend), diff --git a/pkg/config/api.go b/pkg/config/api.go index ec7c4f86d..1368555be 100644 --- a/pkg/config/api.go +++ b/pkg/config/api.go @@ -17,10 +17,10 @@ type ( // Local only config Image string `toml:"-"` KongImage string `toml:"-"` - Port uint16 `toml:"port"` + Port uint16 `toml:"port" validate:"gt=0"` Tls tlsKong `toml:"tls"` // TODO: replace [auth|studio].api_url - ExternalUrl string `toml:"external_url"` + ExternalUrl string `toml:"external_url" validate:"http_url"` } tlsKong struct { diff --git a/pkg/config/auth.go b/pkg/config/auth.go index 28fa7a23d..4a1168c0f 100644 --- a/pkg/config/auth.go +++ b/pkg/config/auth.go @@ -72,15 +72,15 @@ type ( Enabled bool `toml:"enabled"` Image string `toml:"-"` - SiteUrl string `toml:"site_url" mapstructure:"site_url"` - AdditionalRedirectUrls []string `toml:"additional_redirect_urls"` + SiteUrl string `toml:"site_url" mapstructure:"site_url" validate:"http_url"` + AdditionalRedirectUrls []string `toml:"additional_redirect_urls" validate:"dive,http_url"` JwtExpiry uint `toml:"jwt_expiry"` EnableRefreshTokenRotation bool `toml:"enable_refresh_token_rotation"` RefreshTokenReuseInterval uint `toml:"refresh_token_reuse_interval"` EnableManualLinking bool `toml:"enable_manual_linking"` EnableSignup bool `toml:"enable_signup"` EnableAnonymousSignIns bool `toml:"enable_anonymous_sign_ins"` - MinimumPasswordLength uint `toml:"minimum_password_length"` + MinimumPasswordLength uint `toml:"minimum_password_length" validate:"min=6"` PasswordRequirements PasswordRequirements `toml:"password_requirements"` Captcha *captcha `toml:"captcha"` @@ -89,7 +89,7 @@ type ( Sessions sessions `toml:"sessions"` Email email `toml:"email"` Sms sms `toml:"sms"` - External external `toml:"external"` + External external `toml:"external" validate:"dive"` // Custom secrets can be injected from .env file JwtSecret string `toml:"-" mapstructure:"jwt_secret"` @@ -102,29 +102,26 @@ type ( external map[string]provider thirdParty struct { - Firebase tpaFirebase `toml:"firebase"` - Auth0 tpaAuth0 `toml:"auth0"` - Cognito tpaCognito `toml:"aws_cognito"` + Firebase tpaFirebase `toml:"firebase" validate:"excluded_with=Auth0.Enabled Cognito.Enabled"` + Auth0 tpaAuth0 `toml:"auth0" validate:"excluded_with=Firebase.Enabled Cognito.Enabled"` + Cognito tpaCognito `toml:"aws_cognito" validate:"excluded_with=Auth0.Enabled Firebase.Enabled"` } tpaFirebase struct { - Enabled bool `toml:"enabled"` - - ProjectID string `toml:"project_id"` + Enabled bool `toml:"enabled"` + ProjectID string `toml:"project_id" validate:"required_with=Enabled"` } tpaAuth0 struct { - Enabled bool `toml:"enabled"` - - Tenant string `toml:"tenant"` + Enabled bool `toml:"enabled"` + Tenant string `toml:"tenant" validate:"required_with=Enabled"` TenantRegion string `toml:"tenant_region"` } tpaCognito struct { - Enabled bool `toml:"enabled"` - - UserPoolID string `toml:"user_pool_id"` - UserPoolRegion string `toml:"user_pool_region"` + Enabled bool `toml:"enabled"` + UserPoolID string `toml:"user_pool_id" validate:"required_with=Enabled"` + UserPoolRegion string `toml:"user_pool_region" validate:"required_with=Enabled"` } email struct { @@ -132,47 +129,47 @@ type ( DoubleConfirmChanges bool `toml:"double_confirm_changes"` EnableConfirmations bool `toml:"enable_confirmations"` SecurePasswordChange bool `toml:"secure_password_change"` - Template map[string]emailTemplate `toml:"template"` + Template map[string]emailTemplate `toml:"template" validate:"dive"` Smtp *smtp `toml:"smtp"` MaxFrequency time.Duration `toml:"max_frequency"` - OtpLength uint `toml:"otp_length"` + OtpLength uint `toml:"otp_length" validate:"min=6"` OtpExpiry uint `toml:"otp_expiry"` } smtp struct { - Enabled *bool `toml:"enabled"` - Host string `toml:"host"` - Port uint16 `toml:"port"` - User string `toml:"user"` - Pass Secret `toml:"pass"` - AdminEmail string `toml:"admin_email"` + Enabled bool `toml:"enabled"` + Host string `toml:"host" validate:"required_with=Enabled"` + Port uint16 `toml:"port" validate:"required_with=Enabled"` + User string `toml:"user" validate:"required_with=Enabled"` + Pass Secret `toml:"pass" validate:"required_with=Enabled"` + AdminEmail string `toml:"admin_email" validate:"required_with=Enabled"` SenderName string `toml:"sender_name"` } emailTemplate struct { Subject *string `toml:"subject"` - Content *string `toml:"content"` + Content *string `toml:"content" validate:"isdefault"` // Only content path is accepted in config.toml - ContentPath string `toml:"content_path"` + ContentPath string `toml:"content_path" validate:"omitempty,file"` } sms struct { EnableSignup bool `toml:"enable_signup"` EnableConfirmations bool `toml:"enable_confirmations"` - Template string `toml:"template"` - Twilio twilioConfig `toml:"twilio" mapstructure:"twilio"` - TwilioVerify twilioConfig `toml:"twilio_verify" mapstructure:"twilio_verify"` - Messagebird messagebirdConfig `toml:"messagebird" mapstructure:"messagebird"` - Textlocal textlocalConfig `toml:"textlocal" mapstructure:"textlocal"` - Vonage vonageConfig `toml:"vonage" mapstructure:"vonage"` + Template string `toml:"template" validate:"required"` + Twilio twilioConfig `toml:"twilio" mapstructure:"twilio" validate:"excluded_with=TwilioVerify.Enabled Messagebird.Enabled Textlocal.Enabled Vonage.Enabled"` + TwilioVerify twilioConfig `toml:"twilio_verify" mapstructure:"twilio_verify" validate:"excluded_with=Twilio.Enabled Messagebird.Enabled Textlocal.Enabled Vonage.Enabled"` + Messagebird messagebirdConfig `toml:"messagebird" mapstructure:"messagebird" validate:"excluded_with=Twilio.Enabled TwilioVerify.Enabled Textlocal.Enabled Vonage.Enabled"` + Textlocal textlocalConfig `toml:"textlocal" mapstructure:"textlocal" validate:"excluded_with=Twilio.Enabled TwilioVerify.Enabled Messagebird.Enabled Vonage.Enabled"` + Vonage vonageConfig `toml:"vonage" mapstructure:"vonage" validate:"excluded_with=Twilio.Enabled TwilioVerify.Enabled Messagebird.Enabled Textlocal.Enabled"` TestOTP map[string]string `toml:"test_otp"` MaxFrequency time.Duration `toml:"max_frequency"` } captcha struct { Enabled bool `toml:"enabled"` - Provider CaptchaProvider `toml:"provider"` - Secret Secret `toml:"secret"` + Provider CaptchaProvider `toml:"provider" validate:"required_with=Enabled"` + Secret Secret `toml:"secret" validate:"required_with=Enabled"` } hook struct { @@ -185,13 +182,13 @@ type ( factorTypeConfiguration struct { EnrollEnabled bool `toml:"enroll_enabled"` - VerifyEnabled bool `toml:"verify_enabled"` + VerifyEnabled bool `toml:"verify_enabled" validate:"required_with=EnrollEnabled"` } phoneFactorTypeConfiguration struct { factorTypeConfiguration - OtpLength uint `toml:"otp_length"` - Template string `toml:"template"` + OtpLength uint `toml:"otp_length" validate:"min=6"` + Template string `toml:"template" validate:"required"` MaxFrequency time.Duration `toml:"max_frequency"` } @@ -204,7 +201,7 @@ type ( hookConfig struct { Enabled bool `toml:"enabled"` - URI string `toml:"uri"` + URI string `toml:"uri" validate:"required_with=Enabled,http_url|startswith=pg-functions://postgres/"` Secrets Secret `toml:"secrets"` } @@ -215,33 +212,33 @@ type ( twilioConfig struct { Enabled bool `toml:"enabled"` - AccountSid string `toml:"account_sid"` - MessageServiceSid string `toml:"message_service_sid"` - AuthToken Secret `toml:"auth_token" mapstructure:"auth_token"` + AccountSid string `toml:"account_sid" validate:"required_with=Enabled"` + MessageServiceSid string `toml:"message_service_sid" validate:"required_with=Enabled"` + AuthToken Secret `toml:"auth_token" mapstructure:"auth_token" validate:"required_with=Enabled"` } messagebirdConfig struct { Enabled bool `toml:"enabled"` - Originator string `toml:"originator"` - AccessKey Secret `toml:"access_key" mapstructure:"access_key"` + Originator string `toml:"originator" validate:"required_with=Enabled"` + AccessKey Secret `toml:"access_key" mapstructure:"access_key" validate:"required_with=Enabled"` } textlocalConfig struct { Enabled bool `toml:"enabled"` - Sender string `toml:"sender"` - ApiKey Secret `toml:"api_key" mapstructure:"api_key"` + Sender string `toml:"sender" validate:"required_with=Enabled"` + ApiKey Secret `toml:"api_key" mapstructure:"api_key" validate:"required_with=Enabled"` } vonageConfig struct { Enabled bool `toml:"enabled"` - From string `toml:"from"` - ApiKey string `toml:"api_key" mapstructure:"api_key"` - ApiSecret Secret `toml:"api_secret" mapstructure:"api_secret"` + From string `toml:"from" validate:"required_with=Enabled"` + ApiKey string `toml:"api_key" mapstructure:"api_key" validate:"required_with=Enabled"` + ApiSecret Secret `toml:"api_secret" mapstructure:"api_secret" validate:"required_with=Enabled"` } provider struct { Enabled bool `toml:"enabled"` - ClientId string `toml:"client_id"` + ClientId string `toml:"client_id" validate:"required_with=Enabled"` Secret Secret `toml:"secret"` Url string `toml:"url"` RedirectUri string `toml:"redirect_uri"` @@ -558,13 +555,8 @@ func (e *email) fromAuthConfig(remoteConfig v1API.AuthConfigResponse) { } } -func (s smtp) IsEnabled() bool { - // If Enabled is not defined, or defined and set to true - return cast.Val(s.Enabled, true) -} - func (s smtp) toAuthConfigBody(body *v1API.UpdateAuthConfigBody) { - if !s.IsEnabled() { + if !s.Enabled { // Setting a single empty string disables SMTP body.SmtpHost = cast.Ptr("") return @@ -580,14 +572,12 @@ func (s smtp) toAuthConfigBody(body *v1API.UpdateAuthConfigBody) { } func (s *smtp) fromAuthConfig(remoteConfig v1API.AuthConfigResponse) { - showDiff := s.IsEnabled() - // Api resets all values when SMTP is disabled - if enabled := remoteConfig.SmtpHost != nil; s.Enabled != nil { - *s.Enabled = enabled - } - if !showDiff { + // When local config is not set, we assume platform defaults should not change + if s == nil { return } + // Api resets all values when SMTP is disabled + s.Enabled = remoteConfig.SmtpHost != nil s.Host = cast.Val(remoteConfig.SmtpHost, "") s.User = cast.Val(remoteConfig.SmtpUser, "") if len(s.Pass.SHA256) > 0 { diff --git a/pkg/config/auth_test.go b/pkg/config/auth_test.go index df53ba80b..89f7ee66d 100644 --- a/pkg/config/auth_test.go +++ b/pkg/config/auth_test.go @@ -521,7 +521,7 @@ func TestEmailDiff(t *testing.T) { }, }, Smtp: &smtp{ - Enabled: cast.Ptr(true), + Enabled: true, Host: "smtp.sendgrid.net", Port: 587, User: "apikey", @@ -699,7 +699,7 @@ func TestEmailDiff(t *testing.T) { "reauthentication": {}, }, Smtp: &smtp{ - Enabled: cast.Ptr(false), + Enabled: false, Host: "smtp.sendgrid.net", Port: 587, User: "apikey", diff --git a/pkg/config/config.go b/pkg/config/config.go index 4e01c627a..e244a42ea 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -16,6 +16,7 @@ import ( "os" "path" "path/filepath" + "reflect" "regexp" "sort" "strconv" @@ -26,6 +27,7 @@ import ( "github.com/BurntSushi/toml" "github.com/docker/go-units" "github.com/go-errors/errors" + "github.com/go-playground/validator/v10" "github.com/golang-jwt/jwt/v5" "github.com/joho/godotenv" "github.com/mitchellh/mapstructure" @@ -152,7 +154,7 @@ type ( Storage storage `toml:"storage"` Auth auth `toml:"auth" mapstructure:"auth"` EdgeRuntime edgeRuntime `toml:"edge_runtime"` - Functions FunctionConfig `toml:"functions"` + Functions FunctionConfig `toml:"functions" validate:"dive"` Analytics analytics `toml:"analytics"` Experimental experimental `toml:"experimental"` } @@ -165,8 +167,8 @@ type ( realtime struct { Enabled bool `toml:"enabled"` Image string `toml:"-"` - IpVersion AddressFamily `toml:"ip_version"` - MaxHeaderLength uint `toml:"max_header_length"` + IpVersion AddressFamily `toml:"ip_version" validate:"required"` + MaxHeaderLength uint `toml:"max_header_length" validate:"required"` TenantId string `toml:"-"` EncryptionKey string `toml:"-"` SecretKeyBase string `toml:"-"` @@ -175,9 +177,9 @@ type ( studio struct { Enabled bool `toml:"enabled"` Image string `toml:"-"` - Port uint16 `toml:"port"` - ApiUrl string `toml:"api_url"` - OpenaiApiKey string `toml:"openai_api_key"` + Port uint16 `toml:"port" validate:"gt=0"` + ApiUrl string `toml:"api_url" validate:"http_url"` + OpenaiApiKey Secret `toml:"openai_api_key"` PgmetaImage string `toml:"-"` } @@ -187,60 +189,55 @@ type ( Port uint16 `toml:"port"` SmtpPort uint16 `toml:"smtp_port"` Pop3Port uint16 `toml:"pop3_port"` - AdminEmail string `toml:"admin_email"` - SenderName string `toml:"sender_name"` + AdminEmail string `toml:"admin_email" validate:"email"` + SenderName string `toml:"sender_name" validate:"required"` } edgeRuntime struct { Enabled bool `toml:"enabled"` Image string `toml:"-"` - Policy RequestPolicy `toml:"policy"` - InspectorPort uint16 `toml:"inspector_port"` + Policy RequestPolicy `toml:"policy" validate:"required"` + InspectorPort uint16 `toml:"inspector_port" validate:"gt=0"` } FunctionConfig map[string]function function struct { - Enabled *bool `toml:"enabled" json:"-"` + Enabled bool `toml:"enabled" json:"-"` VerifyJWT *bool `toml:"verify_jwt" json:"verifyJWT"` - ImportMap string `toml:"import_map" json:"importMapPath,omitempty"` - Entrypoint string `toml:"entrypoint" json:"entrypointPath,omitempty"` - StaticFiles []string `toml:"static_files" json:"staticFiles,omitempty"` + ImportMap string `toml:"import_map" json:"importMapPath,omitempty" validate:"omitempty,file"` + Entrypoint string `toml:"entrypoint" json:"entrypointPath,omitempty" validate:"file"` + StaticFiles []string `toml:"static_files" json:"staticFiles,omitempty" validate:"dive,filepath"` } analytics struct { Enabled bool `toml:"enabled"` Image string `toml:"-"` VectorImage string `toml:"-"` - Port uint16 `toml:"port"` - Backend LogflareBackend `toml:"backend"` - GcpProjectId string `toml:"gcp_project_id"` - GcpProjectNumber string `toml:"gcp_project_number"` - GcpJwtPath string `toml:"gcp_jwt_path"` + Port uint16 `toml:"port" validate:"gt=0"` + Backend LogflareBackend `toml:"backend" validate:"required"` + GcpProjectId string `toml:"gcp_project_id" validate:"required_if=Enabled true Backend bigquery"` + GcpProjectNumber string `toml:"gcp_project_number" validate:"required_if=Enabled true Backend bigquery"` + GcpJwtPath string `toml:"gcp_jwt_path" validate:"required_if=Enabled true Backend bigquery"` ApiKey string `toml:"-" mapstructure:"api_key"` // Deprecated together with syslog VectorPort uint16 `toml:"vector_port"` } webhooks struct { - Enabled bool `toml:"enabled"` + Enabled bool `toml:"enabled" validate:"required"` } experimental struct { OrioleDBVersion string `toml:"orioledb_version"` - S3Host string `toml:"s3_host"` - S3Region string `toml:"s3_region"` - S3AccessKey string `toml:"s3_access_key"` - S3SecretKey string `toml:"s3_secret_key"` + S3Host string `toml:"s3_host" validate:"required_with=OrioleDBVersion"` + S3Region string `toml:"s3_region" validate:"required_with=OrioleDBVersion"` + S3AccessKey string `toml:"s3_access_key" validate:"required_with=OrioleDBVersion"` + S3SecretKey string `toml:"s3_secret_key" validate:"required_with=OrioleDBVersion"` Webhooks *webhooks `toml:"webhooks"` } ) -func (f function) IsEnabled() bool { - // If Enabled is not defined, or defined and set to true - return f.Enabled == nil || *f.Enabled -} - func (a *auth) Clone() auth { copy := *a if copy.Captcha != nil { @@ -277,9 +274,19 @@ func (a *auth) Clone() auth { return copy } +func (s *storage) Clone() storage { + copy := *s + copy.Buckets = maps.Clone(s.Buckets) + if s.ImageTransformation != nil { + img := *s.ImageTransformation + copy.ImageTransformation = &img + } + return copy +} + func (c *baseConfig) Clone() baseConfig { copy := *c - copy.Storage.Buckets = maps.Clone(c.Storage.Buckets) + copy.Storage = c.Storage.Clone() copy.Functions = maps.Clone(c.Functions) copy.Auth = c.Auth.Clone() if c.Experimental.Webhooks != nil { @@ -445,11 +452,14 @@ func (c *config) loadFromReader(v *viper.Viper, r io.Reader) error { } } // Manually parse [functions.*] to empty struct for backwards compatibility - for key, value := range v.GetStringMap("functions") { - if m, ok := value.(map[string]any); ok && len(m) == 0 { - v.Set("functions."+key, function{}) + for slug := range v.GetStringMap("functions") { + if key := fmt.Sprintf("functions.%s.enabled", slug); !v.IsSet(key) { + v.Set(key, true) } } + if key := "auth.email.smtp"; v.IsSet(key) && !v.IsSet(key+".enabled") { + v.Set(key+".enabled", true) + } if err := v.UnmarshalExact(c, func(dc *mapstructure.DecoderConfig) { dc.TagName = "toml" dc.Squash = true @@ -575,9 +585,51 @@ func (c *config) Load(path string, fsys fs.FS) error { if err := c.baseConfig.resolve(builder, fsys); err != nil { return err } + validate := validator.New(validator.WithRequiredStructEnabled()) + validate.RegisterTagNameFunc(getTagName) + validate.RegisterStructValidation(func(sl validator.StructLevel) { + b := sl.Current().Interface().(bucket) + if s, ok := sl.Parent().Interface().(storage); ok && b.FileSizeLimit > s.FileSizeLimit { + fname := "FileSizeLimit" + bf, _ := sl.Current().Type().FieldByName(fname) + sf, _ := sl.Parent().Type().FieldByName(fname) + limit := fmt.Sprintf( + "%s (= %s.%s)", + units.BytesSize(float64(s.FileSizeLimit)), + sl.Parent().Type().Name(), + getTagName(sf), + ) + sl.ReportError(b.FileSizeLimit, getTagName(bf), fname, "max", limit) + } + }, bucket{}) + if err := validate.Struct(c); err != nil { + // TODO: warn unused env? + if all, ok := err.(validator.ValidationErrors); ok { + var formatted []error + for _, verr := range all { + formatted = append(formatted, errors.Errorf( + "* error decoding '%s': must be %s %s", + strings.TrimPrefix(verr.Namespace(), "config.baseConfig."), + verr.ActualTag(), + verr.Param(), + )) + } + return errors.Join(formatted...) + } + return err + } return c.Validate(fsys) } +func getTagName(field reflect.StructField) string { + name := strings.SplitN(field.Tag.Get("toml"), ",", 2)[0] + // skip if tag key says it should be ignored + if name == "-" { + return "" + } + return name +} + func (c *baseConfig) resolve(builder pathBuilder, fsys fs.FS) error { // Update content paths for name, tmpl := range c.Auth.Email.Template { @@ -894,7 +946,7 @@ func (e *email) validate(fsys fs.FS) (err error) { } e.Template[name] = tmpl } - if e.Smtp != nil && e.Smtp.IsEnabled() { + if e.Smtp != nil && e.Smtp.Enabled { if len(e.Smtp.Host) == 0 { return errors.New("Missing required field in config: auth.email.smtp.host") } diff --git a/pkg/config/db.go b/pkg/config/db.go index eea45f28b..692398510 100644 --- a/pkg/config/db.go +++ b/pkg/config/db.go @@ -69,30 +69,30 @@ type ( db struct { Image string `toml:"-"` - Port uint16 `toml:"port"` - ShadowPort uint16 `toml:"shadow_port"` - MajorVersion uint `toml:"major_version"` + Port uint16 `toml:"port" validate:"gt=0"` + ShadowPort uint16 `toml:"shadow_port" validate:"gt=0"` + MajorVersion uint `toml:"major_version" validate:"min=12,max=15"` Password string `toml:"-"` RootKey string `toml:"-" mapstructure:"root_key"` Pooler pooler `toml:"pooler"` Seed seed `toml:"seed"` Settings settings `toml:"settings"` - Vault map[string]Secret `toml:"vault"` + Vault map[string]Secret `toml:"vault" validate:"dive,required"` } seed struct { Enabled bool `toml:"enabled"` - GlobPatterns []string `toml:"sql_paths"` + GlobPatterns []string `toml:"sql_paths" validate:"dive,filepath"` SqlPaths []string `toml:"-"` } pooler struct { Enabled bool `toml:"enabled"` Image string `toml:"-"` - Port uint16 `toml:"port"` - PoolMode PoolMode `toml:"pool_mode"` - DefaultPoolSize uint `toml:"default_pool_size"` - MaxClientConn uint `toml:"max_client_conn"` + Port uint16 `toml:"port" validate:"gt=0"` + PoolMode PoolMode `toml:"pool_mode" validate:"required"` + DefaultPoolSize uint `toml:"default_pool_size" validate:"required"` + MaxClientConn uint `toml:"max_client_conn" validate:"required"` ConnectionString string `toml:"-"` TenantId string `toml:"-"` EncryptionKey string `toml:"-"` diff --git a/pkg/config/secret.go b/pkg/config/secret.go index eab7f3f00..d83ad5365 100644 --- a/pkg/config/secret.go +++ b/pkg/config/secret.go @@ -64,6 +64,10 @@ func DecryptSecretHookFunc(hashKey string) mapstructure.DecodeHookFunc { if t != reflect.TypeOf(result) { return data, nil } + value := data.(string) + if len(value) == 0 { + return result, nil + } // Get all env vars and filter for DOTENV_PRIVATE_KEY var privateKeys []string for _, env := range os.Environ() { @@ -74,13 +78,12 @@ func DecryptSecretHookFunc(hashKey string) mapstructure.DecodeHookFunc { } } } - // Try each private key var err error privKey := strings.Join(privateKeys, ",") for _, k := range strings.Split(privKey, ",") { // Use the first private key that successfully decrypts the secret - if result.Value, err = decrypt(k, data.(string)); err == nil { + if result.Value, err = decrypt(k, value); err == nil { // Unloaded env() references may be returned verbatim. // Don't hash those values as they are meaningless. if !envPattern.MatchString(result.Value) { diff --git a/pkg/config/storage.go b/pkg/config/storage.go index b026d0cf6..5af047663 100644 --- a/pkg/config/storage.go +++ b/pkg/config/storage.go @@ -8,13 +8,13 @@ import ( type ( storage struct { - Enabled bool `toml:"enabled"` + Enabled bool `toml:"enabled" validate:"required_with=ImageTransformation.Enabled"` Image string `toml:"-"` ImgProxyImage string `toml:"-"` FileSizeLimit sizeInBytes `toml:"file_size_limit"` ImageTransformation *imageTransformation `toml:"image_transformation"` S3Credentials storageS3Credentials `toml:"-"` - Buckets BucketConfig `toml:"buckets"` + Buckets BucketConfig `toml:"buckets" validate:"dive"` } imageTransformation struct { @@ -33,7 +33,7 @@ type ( Public *bool `toml:"public"` FileSizeLimit sizeInBytes `toml:"file_size_limit"` AllowedMimeTypes []string `toml:"allowed_mime_types"` - ObjectsPath string `toml:"objects_path"` + ObjectsPath string `toml:"objects_path" validate:"omitempty,dir|file"` } ) @@ -61,11 +61,7 @@ func (s *storage) FromRemoteStorageConfig(remoteConfig v1API.StorageConfigRespon } func (s *storage) DiffWithRemote(remoteConfig v1API.StorageConfigResponse) ([]byte, error) { - copy := *s - if s.ImageTransformation != nil { - img := *s.ImageTransformation - copy.ImageTransformation = &img - } + copy := s.Clone() // Convert the config values into easily comparable remoteConfig values currentValue, err := ToTomlBytes(copy) if err != nil { diff --git a/pkg/function/batch.go b/pkg/function/batch.go index d06e5bf04..c05a7f6f7 100644 --- a/pkg/function/batch.go +++ b/pkg/function/batch.go @@ -35,7 +35,7 @@ func (s *EdgeRuntimeAPI) UpsertFunctions(ctx context.Context, functionConfig con exists[f.Slug] = struct{}{} } for slug, function := range functionConfig { - if !function.IsEnabled() { + if !function.Enabled { fmt.Fprintln(os.Stderr, "Skipped deploying Function:", slug) continue }