diff --git a/cmd/root.go b/cmd/root.go index d07db1dcb..4a158c56e 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -6,6 +6,7 @@ import ( "os" "strings" + "github.com/mitchellh/mapstructure" "github.com/spf13/afero" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -71,8 +72,18 @@ func Execute() { func init() { cobra.OnInitialize(func() { + // Allow overriding config object with automatic env + // Ref: https://github.com/spf13/viper/issues/761 + envKeysMap := map[string]interface{}{} + dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ + Result: &envKeysMap, + IgnoreUntaggedFields: true, + }) + cobra.CheckErr(err) + cobra.CheckErr(dec.Decode(utils.Config)) + cobra.CheckErr(viper.MergeConfigMap(envKeysMap)) viper.SetEnvPrefix("SUPABASE") - viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) + viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_")) viper.AutomaticEnv() }) diff --git a/internal/functions/serve/serve.go b/internal/functions/serve/serve.go index f88bbac3e..1ceee2736 100644 --- a/internal/functions/serve/serve.go +++ b/internal/functions/serve/serve.go @@ -99,7 +99,7 @@ func Run(ctx context.Context, slug string, envFilePath string, noVerifyJWT *bool }) env := []string{ - "JWT_SECRET=" + utils.JWTSecret, + "JWT_SECRET=" + utils.Config.Auth.JwtSecret, "DENO_ORIGIN=http://localhost:8000", } verifyJWTEnv := "VERIFY_JWT=true" @@ -182,8 +182,8 @@ func Run(ctx context.Context, slug string, envFilePath string, noVerifyJWT *bool env := []string{ "SUPABASE_URL=http://" + utils.KongId + ":8000", - "SUPABASE_ANON_KEY=" + utils.AnonKey, - "SUPABASE_SERVICE_ROLE_KEY=" + utils.ServiceRoleKey, + "SUPABASE_ANON_KEY=" + utils.Config.Auth.AnonKey, + "SUPABASE_SERVICE_ROLE_KEY=" + utils.Config.Auth.ServiceRoleKey, "SUPABASE_DB_URL=postgresql://postgres:postgres@localhost:" + strconv.FormatUint(uint64(utils.Config.Db.Port), 10) + "/postgres", } @@ -264,10 +264,10 @@ func runServeAll(ctx context.Context, envFilePath string, noVerifyJWT *bool, imp }) env := []string{ - "JWT_SECRET=" + utils.JWTSecret, + "JWT_SECRET=" + utils.Config.Auth.JwtSecret, "SUPABASE_URL=http://" + utils.KongId + ":8000", - "SUPABASE_ANON_KEY=" + utils.AnonKey, - "SUPABASE_SERVICE_ROLE_KEY=" + utils.ServiceRoleKey, + "SUPABASE_ANON_KEY=" + utils.Config.Auth.AnonKey, + "SUPABASE_SERVICE_ROLE_KEY=" + utils.Config.Auth.ServiceRoleKey, "SUPABASE_DB_URL=postgresql://postgres:postgres@localhost:" + strconv.FormatUint(uint64(utils.Config.Db.Port), 10) + "/postgres", } verifyJWTEnv := "VERIFY_JWT=true" diff --git a/internal/start/start.go b/internal/start/start.go index a28efdb1e..93c143268 100644 --- a/internal/start/start.go +++ b/internal/start/start.go @@ -103,8 +103,8 @@ func run(p utils.Program, ctx context.Context, fsys afero.Fs, excludedContainers var kongConfigBuf bytes.Buffer if err := kongConfigTemplate.Execute(&kongConfigBuf, struct{ ProjectId, AnonKey, ServiceRoleKey string }{ ProjectId: utils.Config.ProjectId, - AnonKey: utils.AnonKey, - ServiceRoleKey: utils.ServiceRoleKey, + AnonKey: utils.Config.Auth.AnonKey, + ServiceRoleKey: utils.Config.Auth.ServiceRoleKey, }); err != nil { return err } @@ -159,7 +159,7 @@ EOF "GOTRUE_JWT_AUD=authenticated", "GOTRUE_JWT_DEFAULT_GROUP_NAME=authenticated", fmt.Sprintf("GOTRUE_JWT_EXP=%v", utils.Config.Auth.JwtExpiry), - "GOTRUE_JWT_SECRET=" + utils.JWTSecret, + "GOTRUE_JWT_SECRET=" + utils.Config.Auth.JwtSecret, fmt.Sprintf("GOTRUE_EXTERNAL_EMAIL_ENABLED=%v", *utils.Config.Auth.Email.EnableSignup), fmt.Sprintf("GOTRUE_MAILER_SECURE_EMAIL_CHANGE_ENABLED=%v", *utils.Config.Auth.Email.DoubleConfirmChanges), @@ -268,7 +268,7 @@ EOF "DB_NAME=postgres", "DB_AFTER_CONNECT_QUERY=SET search_path TO _realtime", "DB_ENC_KEY=supabaserealtime", - "API_JWT_SECRET=" + utils.JWTSecret, + "API_JWT_SECRET=" + utils.Config.Auth.JwtSecret, "FLY_ALLOC_ID=abc123", "FLY_APP_NAME=realtime", "SECRET_KEY_BASE=EAx3IQ/wRG1v47ZD4NE4/9RzBI8Jmil3x0yhcW4V2NHBP6c2iPIzwjofi2Ep4HIG", @@ -309,7 +309,7 @@ EOF "PGRST_DB_SCHEMAS=" + strings.Join(utils.Config.Api.Schemas, ","), "PGRST_DB_EXTRA_SEARCH_PATH=" + strings.Join(utils.Config.Api.ExtraSearchPath, ","), "PGRST_DB_ANON_ROLE=anon", - "PGRST_JWT_SECRET=" + utils.JWTSecret, + "PGRST_JWT_SECRET=" + utils.Config.Auth.JwtSecret, }, // PostgREST does not expose a shell for health check }, @@ -330,10 +330,10 @@ EOF container.Config{ Image: utils.StorageImage, Env: []string{ - "ANON_KEY=" + utils.AnonKey, - "SERVICE_KEY=" + utils.ServiceRoleKey, + "ANON_KEY=" + utils.Config.Auth.AnonKey, + "SERVICE_KEY=" + utils.Config.Auth.ServiceRoleKey, "POSTGREST_URL=http://" + utils.RestId + ":3000", - "PGRST_JWT_SECRET=" + utils.JWTSecret, + "PGRST_JWT_SECRET=" + utils.Config.Auth.JwtSecret, "DATABASE_URL=postgresql://supabase_storage_admin:postgres@" + utils.DbId + ":5432/postgres", fmt.Sprintf("FILE_SIZE_LIMIT=%v", utils.Config.Storage.FileSizeLimit), "STORAGE_BACKEND=file", @@ -431,8 +431,8 @@ EOF "SUPABASE_URL=http://" + utils.KongId + ":8000", fmt.Sprintf("SUPABASE_REST_URL=http://localhost:%v/rest/v1/", utils.Config.Api.Port), fmt.Sprintf("SUPABASE_PUBLIC_URL=http://localhost:%v/", utils.Config.Api.Port), - "SUPABASE_ANON_KEY=" + utils.AnonKey, - "SUPABASE_SERVICE_KEY=" + utils.ServiceRoleKey, + "SUPABASE_ANON_KEY=" + utils.Config.Auth.AnonKey, + "SUPABASE_SERVICE_KEY=" + utils.Config.Auth.ServiceRoleKey, }, Healthcheck: &container.HealthConfig{ Test: []string{"CMD", "node", "-e", "require('http').get('http://localhost:3000/api/profile', (r) => {if (r.statusCode !== 200) throw new Error(r.statusCode)})"}, diff --git a/internal/status/status.go b/internal/status/status.go index 8ea7bf461..ccf75f500 100644 --- a/internal/status/status.go +++ b/internal/status/status.go @@ -46,9 +46,9 @@ func (c *CustomName) toValues(exclude ...string) map[string]string { values[c.StudioURL] = fmt.Sprintf("http://localhost:%d", utils.Config.Studio.Port) } if !sliceContains(exclude, utils.GotrueId) && !sliceContains(exclude, utils.ShortContainerImageName(utils.GotrueImage)) { - values[c.JWTSecret] = utils.JWTSecret - values[c.AnonKey] = utils.AnonKey - values[c.ServiceRoleKey] = utils.ServiceRoleKey + values[c.JWTSecret] = utils.Config.Auth.JwtSecret + values[c.AnonKey] = utils.Config.Auth.AnonKey + values[c.ServiceRoleKey] = utils.Config.Auth.ServiceRoleKey } if !sliceContains(exclude, utils.InbucketId) && !sliceContains(exclude, utils.ShortContainerImageName(utils.InbucketImage)) { values[c.InbucketURL] = fmt.Sprintf("http://localhost:%d", utils.Config.Inbucket.Port) @@ -138,7 +138,7 @@ func isPostgRESTHealthy(ctx context.Context) bool { if err != nil { return false } - req.Header.Add("apikey", utils.AnonKey) + req.Header.Add("apikey", utils.Config.Auth.AnonKey) resp, err := http.DefaultClient.Do(req) return err == nil && resp.StatusCode == http.StatusOK } diff --git a/internal/utils/config.go b/internal/utils/config.go index ba3a04e1e..caae461f3 100644 --- a/internal/utils/config.go +++ b/internal/utils/config.go @@ -11,7 +11,9 @@ import ( "github.com/BurntSushi/toml" "github.com/docker/go-units" + "github.com/joho/godotenv" "github.com/spf13/afero" + "github.com/spf13/viper" ) var ( @@ -84,7 +86,7 @@ type ( Studio studio `toml:"studio"` Inbucket inbucket `toml:"inbucket"` Storage storage `toml:"storage"` - Auth auth `toml:"auth"` + Auth auth `toml:"auth" mapstructure:"auth"` Functions map[string]function `toml:"functions"` // TODO // Scripts scripts @@ -124,6 +126,10 @@ type ( EnableSignup *bool `toml:"enable_signup"` Email email `toml:"email"` External map[string]provider + // Custom secrets can be injected from .env file + JwtSecret string `toml:"-" mapstructure:"jwt_secret"` + AnonKey string `toml:"-" mapstructure:"anon_key"` + ServiceRoleKey string `toml:"-" mapstructure:"service_role_key"` } email struct { @@ -166,6 +172,13 @@ func LoadConfigFS(fsys afero.Fs) error { } return fmt.Errorf("cannot read config in %s: %w", cwd, err) } + // Load secrets from .env file + if err := godotenv.Load(); err != nil && !errors.Is(err, os.ErrNotExist) { + return err + } + if err := viper.Unmarshal(&Config); err != nil { + return err + } // Process decoded TOML. { @@ -236,6 +249,15 @@ func LoadConfigFS(fsys afero.Fs) error { if Config.Auth.JwtExpiry == 0 { Config.Auth.JwtExpiry = 3600 } + if Config.Auth.JwtSecret == "" { + Config.Auth.JwtSecret = "super-secret-jwt-token-with-at-least-32-characters-long" + } + if Config.Auth.AnonKey == "" { + Config.Auth.AnonKey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0" + } + if Config.Auth.ServiceRoleKey == "" { + Config.Auth.ServiceRoleKey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU" + } if Config.Auth.EnableSignup == nil { x := true Config.Auth.EnableSignup = &x diff --git a/internal/utils/misc.go b/internal/utils/misc.go index 0a01f3289..fe9db8a16 100644 --- a/internal/utils/misc.go +++ b/internal/utils/misc.go @@ -82,9 +82,6 @@ SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '%[1]s'; DO 'BEGIN WHILE ( SELECT COUNT(*) FROM pg_replication_slots WHERE database = ''%[1]s'' ) > 0 LOOP END LOOP; END';` - AnonKey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0" - ServiceRoleKey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU" - JWTSecret = "super-secret-jwt-token-with-at-least-32-characters-long" AccessTokenKey = "access-token" )