• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

supabase / cli / 4476643223

21 Mar 2023 07:48AM UTC coverage: 62.608% (+0.005%) from 62.603%
4476643223

Pull #938

github

Qiao Han
chore: add comments to umbrella issue
Pull Request #938: feat: removes harded coded value for jwt secret, service role key, and anon key

46 of 46 new or added lines in 5 files covered. (100.0%)

3826 of 6111 relevant lines covered (62.61%)

1205.32 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

67.27
/internal/utils/config.go
1
package utils
2

3
import (
4
        _ "embed"
5
        "errors"
6
        "fmt"
7
        "os"
8
        "path/filepath"
9
        "regexp"
10
        "text/template"
11

12
        "github.com/BurntSushi/toml"
13
        "github.com/docker/go-units"
14
        "github.com/joho/godotenv"
15
        "github.com/spf13/afero"
16
        "github.com/spf13/viper"
17
)
18

19
var (
20
        DbImage     string
21
        NetId       string
22
        DbId        string
23
        KongId      string
24
        GotrueId    string
25
        InbucketId  string
26
        RealtimeId  string
27
        RestId      string
28
        StorageId   string
29
        ImgProxyId  string
30
        DifferId    string
31
        PgmetaId    string
32
        StudioId    string
33
        DenoRelayId string
34

35
        InitialSchemaSql string
36
        //go:embed templates/initial_schemas/13.sql
37
        InitialSchemaPg13Sql string
38
        //go:embed templates/initial_schemas/14.sql
39
        InitialSchemaPg14Sql string
40
        //go:embed templates/initial_schemas/15.sql
41
        InitialSchemaPg15Sql string
42

43
        authExternalProviders = []string{
44
                "apple",
45
                "azure",
46
                "bitbucket",
47
                "discord",
48
                "facebook",
49
                "github",
50
                "gitlab",
51
                "google",
52
                "keycloak",
53
                "linkedin",
54
                "notion",
55
                "twitch",
56
                "twitter",
57
                "slack",
58
                "spotify",
59
                "workos",
60
                "zoom",
61
        }
62

63
        //go:embed templates/init_config.toml
64
        initConfigEmbed    string
65
        initConfigTemplate = template.Must(template.New("initConfig").Parse(initConfigEmbed))
66
)
67

68
// Type for turning human-friendly bytes string ("5MB", "32kB") into an int64 during toml decoding.
69
type sizeInBytes int64
70

71
func (s *sizeInBytes) UnmarshalText(text []byte) error {
54✔
72
        size, err := units.RAMInBytes(string(text))
54✔
73
        if err == nil {
107✔
74
                *s = sizeInBytes(size)
53✔
75
        }
53✔
76
        return err
54✔
77
}
78

79
var Config config
80

81
type (
82
        config struct {
83
                ProjectId string              `toml:"project_id"`
84
                Api       api                 `toml:"api"`
85
                Db        db                  `toml:"db"`
86
                Studio    studio              `toml:"studio"`
87
                Inbucket  inbucket            `toml:"inbucket"`
88
                Storage   storage             `toml:"storage"`
89
                Auth      auth                `toml:"auth" mapstructure:"auth"`
90
                Functions map[string]function `toml:"functions"`
91
                // TODO
92
                // Scripts   scripts
93
        }
94

95
        api struct {
96
                Port            uint     `toml:"port"`
97
                Schemas         []string `toml:"schemas"`
98
                ExtraSearchPath []string `toml:"extra_search_path"`
99
                MaxRows         uint     `toml:"max_rows"`
100
        }
101

102
        db struct {
103
                Port         uint `toml:"port"`
104
                ShadowPort   uint `toml:"shadow_port"`
105
                MajorVersion uint `toml:"major_version"`
106
        }
107

108
        studio struct {
109
                Port uint `toml:"port"`
110
        }
111

112
        inbucket struct {
113
                Port     uint `toml:"port"`
114
                SmtpPort uint `toml:"smtp_port"`
115
                Pop3Port uint `toml:"pop3_port"`
116
        }
117

118
        storage struct {
119
                FileSizeLimit sizeInBytes `toml:"file_size_limit"`
120
        }
121

122
        auth struct {
123
                SiteUrl                string   `toml:"site_url"`
124
                AdditionalRedirectUrls []string `toml:"additional_redirect_urls"`
125
                JwtExpiry              uint     `toml:"jwt_expiry"`
126
                EnableSignup           *bool    `toml:"enable_signup"`
127
                Email                  email    `toml:"email"`
128
                External               map[string]provider
129
                // Custom secrets can be injected from .env file
130
                JwtSecret      string `toml:"-" mapstructure:"jwt_secret"`
131
                AnonKey        string `toml:"-" mapstructure:"anon_key"`
132
                ServiceRoleKey string `toml:"-" mapstructure:"service_role_key"`
133
        }
134

135
        email struct {
136
                EnableSignup         *bool `toml:"enable_signup"`
137
                DoubleConfirmChanges *bool `toml:"double_confirm_changes"`
138
                EnableConfirmations  *bool `toml:"enable_confirmations"`
139
        }
140

141
        provider struct {
142
                Enabled     bool   `toml:"enabled"`
143
                ClientId    string `toml:"client_id"`
144
                Secret      string `toml:"secret"`
145
                Url         string `toml:"url"`
146
                RedirectUri string `toml:"redirect_uri"`
147
        }
148

149
        function struct {
150
                VerifyJWT *bool  `toml:"verify_jwt"`
151
                ImportMap string `toml:"import_map"`
152
        }
153

154
        // TODO
155
        // scripts struct {
156
        //         BeforeMigrations string `toml:"before_migrations"`
157
        //         AfterMigrations  string `toml:"after_migrations"`
158
        // }
159
)
160

161
func LoadConfig() error {
×
162
        return LoadConfigFS(afero.NewOsFs())
×
163
}
×
164

165
func LoadConfigFS(fsys afero.Fs) error {
74✔
166
        // TODO: provide a config interface for all sub commands to use fsys
74✔
167
        if _, err := toml.DecodeFS(afero.NewIOFS(fsys), ConfigPath, &Config); err != nil {
99✔
168
                CmdSuggestion = fmt.Sprintf("Have you set up the project with %s?", Aqua("supabase init"))
25✔
169
                cwd, osErr := os.Getwd()
25✔
170
                if osErr != nil {
25✔
171
                        cwd = "current directory"
×
172
                }
×
173
                return fmt.Errorf("cannot read config in %s: %w", cwd, err)
25✔
174
        }
175
        // Load secrets from .env file
176
        if err := godotenv.Load(); err != nil && !errors.Is(err, os.ErrNotExist) {
49✔
177
                return err
×
178
        }
×
179
        if err := viper.Unmarshal(&Config); err != nil {
49✔
180
                return err
×
181
        }
×
182

183
        // Process decoded TOML.
184
        {
49✔
185
                if Config.ProjectId == "" {
49✔
186
                        return errors.New("Missing required field in config: project_id")
×
187
                } else {
49✔
188
                        NetId = "supabase_network_" + Config.ProjectId
49✔
189
                        DbId = "supabase_db_" + Config.ProjectId
49✔
190
                        KongId = "supabase_kong_" + Config.ProjectId
49✔
191
                        GotrueId = "supabase_auth_" + Config.ProjectId
49✔
192
                        InbucketId = "supabase_inbucket_" + Config.ProjectId
49✔
193
                        RealtimeId = "realtime-dev.supabase_realtime_" + Config.ProjectId
49✔
194
                        RestId = "supabase_rest_" + Config.ProjectId
49✔
195
                        StorageId = "supabase_storage_" + Config.ProjectId
49✔
196
                        ImgProxyId = "storage_imgproxy_" + Config.ProjectId
49✔
197
                        DifferId = "supabase_differ_" + Config.ProjectId
49✔
198
                        PgmetaId = "supabase_pg_meta_" + Config.ProjectId
49✔
199
                        StudioId = "supabase_studio_" + Config.ProjectId
49✔
200
                        DenoRelayId = "supabase_deno_relay_" + Config.ProjectId
49✔
201
                }
49✔
202
                if Config.Api.Port == 0 {
49✔
203
                        return errors.New("Missing required field in config: api.port")
×
204
                }
×
205
                if Config.Api.MaxRows == 0 {
49✔
206
                        Config.Api.MaxRows = 1000
×
207
                }
×
208
                if len(Config.Api.Schemas) == 0 {
49✔
209
                        Config.Api.Schemas = []string{"public", "storage", "graphql_public"}
×
210
                }
×
211
                // Append required schemas if they are missing
212
                Config.Api.Schemas = removeDuplicates(append([]string{"public", "storage"}, Config.Api.Schemas...))
49✔
213
                Config.Api.ExtraSearchPath = removeDuplicates(append([]string{"public"}, Config.Api.ExtraSearchPath...))
49✔
214
                if Config.Db.Port == 0 {
49✔
215
                        return errors.New("Missing required field in config: db.port")
×
216
                }
×
217
                if Config.Db.ShadowPort == 0 {
66✔
218
                        Config.Db.ShadowPort = 54320
17✔
219
                }
17✔
220
                switch Config.Db.MajorVersion {
49✔
221
                case 0:
×
222
                        return errors.New("Missing required field in config: db.major_version")
×
223
                case 12:
×
224
                        return errors.New("Postgres version 12.x is unsupported. To use the CLI, either start a new project or follow project migration steps here: https://supabase.com/docs/guides/database#migrating-between-projects.")
×
225
                case 13:
×
226
                        DbImage = Pg13Image
×
227
                        InitialSchemaSql = InitialSchemaPg13Sql
×
228
                case 14:
×
229
                        DbImage = Pg14Image
×
230
                        InitialSchemaSql = InitialSchemaPg14Sql
×
231
                case 15:
49✔
232
                        DbImage = Pg15Image
49✔
233
                        InitialSchemaSql = InitialSchemaPg15Sql
49✔
234
                default:
×
235
                        return fmt.Errorf("Failed reading config: Invalid %s: %v.", Aqua("db.major_version"), Config.Db.MajorVersion)
×
236
                }
237
                if Config.Studio.Port == 0 {
49✔
238
                        return errors.New("Missing required field in config: studio.port")
×
239
                }
×
240
                if Config.Inbucket.Port == 0 {
49✔
241
                        return errors.New("Missing required field in config: inbucket.port")
×
242
                }
×
243
                if Config.Storage.FileSizeLimit == 0 {
49✔
244
                        Config.Storage.FileSizeLimit = 50 * units.MiB
×
245
                }
×
246
                if Config.Auth.SiteUrl == "" {
49✔
247
                        return errors.New("Missing required field in config: auth.site_url")
×
248
                }
×
249
                if Config.Auth.JwtExpiry == 0 {
49✔
250
                        Config.Auth.JwtExpiry = 3600
×
251
                }
×
252
                if Config.Auth.JwtSecret == "" {
66✔
253
                        Config.Auth.JwtSecret = "super-secret-jwt-token-with-at-least-32-characters-long"
17✔
254
                }
17✔
255
                if Config.Auth.AnonKey == "" {
66✔
256
                        Config.Auth.AnonKey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0"
17✔
257
                }
17✔
258
                if Config.Auth.ServiceRoleKey == "" {
66✔
259
                        Config.Auth.ServiceRoleKey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU"
17✔
260
                }
17✔
261
                if Config.Auth.EnableSignup == nil {
49✔
262
                        x := true
×
263
                        Config.Auth.EnableSignup = &x
×
264
                }
×
265
                if Config.Auth.Email.EnableSignup == nil {
49✔
266
                        x := true
×
267
                        Config.Auth.Email.EnableSignup = &x
×
268
                }
×
269
                if Config.Auth.Email.DoubleConfirmChanges == nil {
49✔
270
                        x := true
×
271
                        Config.Auth.Email.DoubleConfirmChanges = &x
×
272
                }
×
273
                if Config.Auth.Email.EnableConfirmations == nil {
49✔
274
                        x := true
×
275
                        Config.Auth.Email.EnableConfirmations = &x
×
276
                }
×
277
                if Config.Auth.External == nil {
49✔
278
                        Config.Auth.External = map[string]provider{}
×
279
                }
×
280

281
                for _, ext := range authExternalProviders {
867✔
282
                        if _, ok := Config.Auth.External[ext]; !ok {
1,090✔
283
                                Config.Auth.External[ext] = provider{
272✔
284
                                        Enabled:  false,
272✔
285
                                        ClientId: "",
272✔
286
                                        Secret:   "",
272✔
287
                                }
272✔
288
                        } else if Config.Auth.External[ext].Enabled {
820✔
289
                                maybeLoadEnv := func(s string) (string, error) {
6✔
290
                                        matches := regexp.MustCompile(`^env\((.*)\)$`).FindStringSubmatch(s)
4✔
291
                                        if len(matches) == 0 {
5✔
292
                                                return s, nil
1✔
293
                                        }
1✔
294

295
                                        envName := matches[1]
3✔
296
                                        value := os.Getenv(envName)
3✔
297
                                        if value == "" {
4✔
298
                                                return "", errors.New(`Error evaluating "env(` + envName + `)": environment variable ` + envName + " is unset.")
1✔
299
                                        }
1✔
300

301
                                        return value, nil
2✔
302
                                }
303

304
                                var clientId, secret, redirectUri, url string
2✔
305

2✔
306
                                if Config.Auth.External[ext].ClientId == "" {
2✔
307
                                        return fmt.Errorf("Missing required field in config: auth.external.%s.client_id", ext)
×
308
                                } else {
2✔
309
                                        v, err := maybeLoadEnv(Config.Auth.External[ext].ClientId)
2✔
310
                                        if err != nil {
3✔
311
                                                return err
1✔
312
                                        }
1✔
313
                                        clientId = v
1✔
314
                                }
315
                                if Config.Auth.External[ext].Secret == "" {
1✔
316
                                        return fmt.Errorf("Missing required field in config: auth.external.%s.secret", ext)
×
317
                                } else {
1✔
318
                                        v, err := maybeLoadEnv(Config.Auth.External[ext].Secret)
1✔
319
                                        if err != nil {
1✔
320
                                                return err
×
321
                                        }
×
322
                                        secret = v
1✔
323
                                }
324

325
                                if Config.Auth.External[ext].RedirectUri != "" {
1✔
326
                                        v, err := maybeLoadEnv(Config.Auth.External[ext].RedirectUri)
×
327
                                        if err != nil {
×
328
                                                return err
×
329
                                        }
×
330
                                        redirectUri = v
×
331
                                }
332

333
                                if Config.Auth.External[ext].Url != "" {
2✔
334
                                        v, err := maybeLoadEnv(Config.Auth.External[ext].Url)
1✔
335
                                        if err != nil {
1✔
336
                                                return err
×
337
                                        }
×
338
                                        url = v
1✔
339
                                }
340

341
                                Config.Auth.External[ext] = provider{
1✔
342
                                        Enabled:     true,
1✔
343
                                        ClientId:    clientId,
1✔
344
                                        Secret:      secret,
1✔
345
                                        RedirectUri: redirectUri,
1✔
346
                                        Url:         url,
1✔
347
                                }
1✔
348
                        }
349
                }
350
        }
351

352
        if Config.Functions == nil {
64✔
353
                Config.Functions = map[string]function{}
16✔
354
        }
16✔
355
        for name, functionConfig := range Config.Functions {
51✔
356
                verifyJWT := functionConfig.VerifyJWT
3✔
357

3✔
358
                if verifyJWT == nil {
3✔
359
                        x := true
×
360
                        verifyJWT = &x
×
361
                }
×
362

363
                Config.Functions[name] = function{
3✔
364
                        VerifyJWT: verifyJWT,
3✔
365
                        ImportMap: functionConfig.ImportMap,
3✔
366
                }
3✔
367
        }
368

369
        return nil
48✔
370
}
371

372
func InitConfig(projectId string, fsys afero.Fs) error {
57✔
373
        // Defaults to current directory name as project id
57✔
374
        if len(projectId) == 0 {
110✔
375
                cwd, err := os.Getwd()
53✔
376
                if err != nil {
53✔
377
                        return err
×
378
                }
×
379
                projectId = filepath.Base(cwd)
53✔
380
        }
381
        // Create config file
382
        if err := MkdirIfNotExistFS(fsys, filepath.Dir(ConfigPath)); err != nil {
58✔
383
                return err
1✔
384
        }
1✔
385
        f, err := fsys.OpenFile(ConfigPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
56✔
386
        if err != nil {
56✔
387
                return err
×
388
        }
×
389
        defer f.Close()
56✔
390
        // Update from template
56✔
391
        return initConfigTemplate.Execute(f, struct{ ProjectId string }{
56✔
392
                ProjectId: projectId,
56✔
393
        })
56✔
394
}
395

396
func WriteConfig(fsys afero.Fs, _test bool) error {
53✔
397
        return InitConfig("", fsys)
53✔
398
}
53✔
399

400
func removeDuplicates(slice []string) (result []string) {
98✔
401
        set := make(map[string]struct{})
98✔
402
        for _, item := range slice {
490✔
403
                if _, exists := set[item]; !exists {
637✔
404
                        set[item] = struct{}{}
245✔
405
                        result = append(result, item)
245✔
406
                }
245✔
407
        }
408
        return result
98✔
409
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc