Repository: supabase/cli
Pull request #786 (2023-01-11 13:37:58)

feat: support `verify_jwt` functions config
## What kind of change does this PR introduce?

Feature

## What is the current behavior?

Need to set `--no-verify-jwt` flag every time `functions serve` and `functions deploy` commands are run.

## What is the new behavior?

Add per-function config to allow setting `verify_jwt`. `verify_jwt` value falls back to the config is the flag is not set - if it's set, it overrides the config.

## Additional context

Closes https://github.com/supabase/cli/issues/280


coveralls: ## Pull Request Test Coverage Report for [Build 3900096761](https://coveralls.io/builds/55878378)

* **27** of **45**   **(60.0%)**  changed or added relevant lines in **4** files are covered.
* **1** unchanged line in **1** file lost coverage.
* Overall coverage decreased (**-0.01%**) to **62.834%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [internal/utils/config.go](https://coveralls.io/builds/55878378/source?filename=internal%2Futils%2Fconfig.go#L338) | 10 | 13 | 76.92%
| [internal/functions/serve/serve.go](https://coveralls.io/builds/55878378/source?filename=internal%2Ffunctions%2Fserve%2Fserve.go#L90) | 5 | 9 | 55.56%
| [cmd/functions.go](https://coveralls.io/builds/55878378/source?filename=cmd%2Ffunctions.go#L63) | 2 | 13 | 15.38%
<!-- | **Total:** | **27** | **45** | **60.0%** | -->

|  Files with Coverage Reduction | New Missed Lines | % |
| :-----|--------------|--: |
| [internal/functions/serve/serve.go](https://coveralls.io/builds/55878378/source?filename=internal%2Ffunctions%2Fserve%2Fserve.go#L94) | 1 | 44.59% |
<!-- | **Total:** | **1** |  | -->

|  Totals | [![Coverage Status](https://coveralls.io/builds/55878378/badge)](https://coveralls.io/builds/55878378) |
| :-- | --: |
| Change from base [Build 3893621394](https://coveralls.io/builds/55850420): |  -0.01% |
| Covered Lines: | 3601 |
| Relevant Lines: | 5731 |

---
##### 💛  - [Coveralls](https://coveralls.io)


Patch:
diff --git a/cmd/functions.go b/cmd/functions.go
index 59a9f473..5a58651a 100644
--- a/cmd/functions.go
+++ b/cmd/functions.go
@@ -12,7 +12,7 @@ import (
 	"github.com/spf13/cobra"
 	"github.com/supabase/cli/internal/functions/delete"
 	"github.com/supabase/cli/internal/functions/deploy"
-	"github.com/supabase/cli/internal/functions/new"
+	new_ "github.com/supabase/cli/internal/functions/new"
 	"github.com/supabase/cli/internal/functions/serve"
 	"github.com/supabase/cli/internal/login"
 	"github.com/supabase/cli/internal/utils"
@@ -42,7 +42,7 @@ var (
 		},
 	}
 
-	noVerifyJWT     bool
+	noVerifyJWT     = new(bool)
 	useLegacyBundle bool
 	importMapPath   string
 
@@ -60,7 +60,11 @@ var (
 				return err
 			}
 			ctx, _ := signal.NotifyContext(cmd.Context(), os.Interrupt)
-			return deploy.Run(ctx, args[0], projectRef, !noVerifyJWT, useLegacyBundle, importMapPath, fsys)
+			// Fallback to config if user did not set the flag.
+			if !cmd.Flags().Changed("no-verify-jwt") {
+				noVerifyJWT = nil
+			}
+			return deploy.Run(ctx, args[0], projectRef, noVerifyJWT, useLegacyBundle, importMapPath, fsys)
 		},
 	}
 
@@ -70,7 +74,7 @@ var (
 		Args:  cobra.ExactArgs(1),
 		RunE: func(cmd *cobra.Command, args []string) error {
 			ctx, _ := signal.NotifyContext(cmd.Context(), os.Interrupt)
-			return new.Run(ctx, args[0], afero.NewOsFs())
+			return new_.Run(ctx, args[0], afero.NewOsFs())
 		},
 	}
 
@@ -82,18 +86,22 @@ var (
 		Args:  cobra.ExactArgs(1),
 		RunE: func(cmd *cobra.Command, args []string) error {
 			ctx, _ := signal.NotifyContext(cmd.Context(), os.Interrupt)
-			return serve.Run(ctx, args[0], envFilePath, !noVerifyJWT, importMapPath, afero.NewOsFs())
+			// Fallback to config if user did not set the flag.
+			if !cmd.Flags().Changed("no-verify-jwt") {
+				noVerifyJWT = nil
+			}
+			return serve.Run(ctx, args[0], envFilePath, noVerifyJWT, importMapPath, afero.NewOsFs())
 		},
 	}
 )
 
 func init() {
 	functionsDeleteCmd.Flags().StringVar(&projectRef, "project-ref", "", "Project ref of the Supabase project.")
-	functionsDeployCmd.Flags().BoolVar(&noVerifyJWT, "no-verify-jwt", false, "Disable JWT verification for the Function.")
+	functionsDeployCmd.Flags().BoolVar(noVerifyJWT, "no-verify-jwt", false, "Disable JWT verification for the Function.")
 	functionsDeployCmd.Flags().StringVar(&projectRef, "project-ref", "", "Project ref of the Supabase project.")
 	functionsDeployCmd.Flags().BoolVar(&useLegacyBundle, "legacy-bundle", false, "Use legacy bundling mechanism.")
 	functionsDeployCmd.Flags().StringVar(&importMapPath, "import-map", "", "Path to import map file.")
-	functionsServeCmd.Flags().BoolVar(&noVerifyJWT, "no-verify-jwt", false, "Disable JWT verification for the Function.")
+	functionsServeCmd.Flags().BoolVar(noVerifyJWT, "no-verify-jwt", false, "Disable JWT verification for the Function.")
 	functionsServeCmd.Flags().StringVar(&envFilePath, "env-file", "", "Path to an env file to be populated to the Function environment.")
 	functionsServeCmd.Flags().StringVar(&importMapPath, "import-map", "", "Path to import map file.")
 	functionsCmd.AddCommand(functionsDeleteCmd)
diff --git a/internal/functions/deploy/deploy.go b/internal/functions/deploy/deploy.go
index 1667144f..1783862e 100644
--- a/internal/functions/deploy/deploy.go
+++ b/internal/functions/deploy/deploy.go
@@ -20,7 +20,7 @@ import (
 
 const EszipContentType = "application/vnd.denoland.eszip"
 
-func Run(ctx context.Context, slug string, projectRefArg string, verifyJWT bool, useLegacyBundle bool, importMapPath string, fsys afero.Fs) error {
+func Run(ctx context.Context, slug string, projectRefArg string, noVerifyJWT *bool, useLegacyBundle bool, importMapPath string, fsys afero.Fs) error {
 	// 1. Sanity checks.
 	projectRef := projectRefArg
 	var buildScriptPath string
@@ -34,6 +34,17 @@ func Run(ctx context.Context, slug string, projectRefArg string, verifyJWT bool,
 		} else if !utils.ProjectRefPattern.MatchString(projectRef) {
 			return errors.New("Invalid project ref format. Must be like `abcdefghijklmnopqrst`.")
 		}
+		// Ensure noVerifyJWT is not nil.
+		if noVerifyJWT == nil {
+			x := false
+			// Load function config if any, but continue on error.
+			if err := utils.LoadConfigFS(fsys); err == nil {
+				if functionConfig, ok := utils.Config.Functions[slug]; ok && !*functionConfig.VerifyJWT {
+					x = true
+				}
+			}
+			noVerifyJWT = &x
+		}
 		if err := utils.ValidateFunctionSlug(slug); err != nil {
 			return err
 		}
@@ -86,7 +97,7 @@ func Run(ctx context.Context, slug string, projectRefArg string, verifyJWT bool,
 
 	// 3. Deploy new Function.
 	fmt.Println("Deploying " + utils.Bold(slug) + " (script size: " + utils.Bold(units.HumanSize(float64(functionSize))) + ")")
-	return deployFunction(ctx, projectRef, slug, functionBody, verifyJWT, useLegacyBundle)
+	return deployFunction(ctx, projectRef, slug, functionBody, !*noVerifyJWT, useLegacyBundle)
 }
 
 func makeLegacyFunctionBody(functionBody io.Reader) (string, error) {
diff --git a/internal/functions/serve/serve.go b/internal/functions/serve/serve.go
index d738156b..b2c2438a 100644
--- a/internal/functions/serve/serve.go
+++ b/internal/functions/serve/serve.go
@@ -40,7 +40,7 @@ func ParseEnvFile(envFilePath string) ([]string, error) {
 	return env, nil
 }
 
-func Run(ctx context.Context, slug string, envFilePath string, verifyJWT bool, importMapPath string, fsys afero.Fs) error {
+func Run(ctx context.Context, slug string, envFilePath string, noVerifyJWT *bool, importMapPath string, fsys afero.Fs) error {
 	// 1. Sanity checks.
 	{
 		if err := utils.AssertSupabaseCliIsSetUpFS(fsys); err != nil {
@@ -84,11 +84,15 @@ func Run(ctx context.Context, slug string, envFilePath string, verifyJWT bool, i
 			"JWT_SECRET=" + utils.JWTSecret,
 			"DENO_ORIGIN=http://localhost:8000",
 		}
-		if verifyJWT {
-			env = append(env, "VERIFY_JWT=true")
-		} else {
-			env = append(env, "VERIFY_JWT=false")
+		verifyJWTEnv := "VERIFY_JWT=true"
+		if noVerifyJWT == nil {
+			if functionConfig, ok := utils.Config.Functions[slug]; ok && !*functionConfig.VerifyJWT {
+				verifyJWTEnv = "VERIFY_JWT=false"
+			}
+		} else if *noVerifyJWT {
+			verifyJWTEnv = "VERIFY_JWT=false"
 		}
+		env = append(env, verifyJWTEnv)
 
 		cwd, err := os.Getwd()
 		if err != nil {
diff --git a/internal/utils/config.go b/internal/utils/config.go
index 7c966321..eea9415e 100644
--- a/internal/utils/config.go
+++ b/internal/utils/config.go
@@ -83,13 +83,14 @@ var Config config
 
 type (
 	config struct {
-		ProjectId string   `toml:"project_id"`
-		Api       api      `toml:"api"`
-		Db        db       `toml:"db"`
-		Studio    studio   `toml:"studio"`
-		Inbucket  inbucket `toml:"inbucket"`
-		Storage   storage  `toml:"storage"`
-		Auth      auth     `toml:"auth"`
+		ProjectId string              `toml:"project_id"`
+		Api       api                 `toml:"api"`
+		Db        db                  `toml:"db"`
+		Studio    studio              `toml:"studio"`
+		Inbucket  inbucket            `toml:"inbucket"`
+		Storage   storage             `toml:"storage"`
+		Auth      auth                `toml:"auth"`
+		Functions map[string]function `toml:"functions"`
 		// TODO
 		// Scripts   scripts
 	}
@@ -144,6 +145,10 @@ type (
 		RedirectUri string `toml:"redirect_uri"`
 	}
 
+	function struct {
+		VerifyJWT *bool `toml:"verify_jwt"`
+	}
+
 	// TODO
 	// scripts struct {
 	// 	BeforeMigrations string `toml:"before_migrations"`
@@ -323,6 +328,22 @@ func LoadConfigFS(fsys afero.Fs) error {
 		}
 	}
 
+	if Config.Functions == nil {
+		Config.Functions = map[string]function{}
+	}
+	for name, functionConfig := range Config.Functions {
+		verifyJWT := functionConfig.VerifyJWT
+
+		if verifyJWT == nil {
+			x := true
+			verifyJWT = &x
+		}
+
+		Config.Functions[name] = function{
+			VerifyJWT: verifyJWT,
+		}
+	}
+
 	return nil
 }
 


Test patch:
diff --git a/internal/functions/deploy/deploy_test.go b/internal/functions/deploy/deploy_test.go
index 74445f33..d280b7ab 100644
--- a/internal/functions/deploy/deploy_test.go
+++ b/internal/functions/deploy/deploy_test.go
@@ -4,8 +4,10 @@ import (
 	"archive/zip"
 	"bytes"
 	"context"
+	"encoding/json"
 	"errors"
 	"fmt"
+	"io"
 	"log"
 	"net/http"
 	"os"
@@ -63,7 +65,8 @@ func TestDeployCommand(t *testing.T) {
 			Reply(http.StatusCreated).
 			JSON(api.FunctionResponse{Id: "1"})
 		// Run test
-		assert.NoError(t, Run(context.Background(), slug, project, false, true, "", fsys))
+		noVerifyJWT := true
+		assert.NoError(t, Run(context.Background(), slug, project, &noVerifyJWT, true, "", fsys))
 		// Validate api
 		assert.Empty(t, apitest.ListUnmatchedRequests())
 	})
@@ -90,7 +93,8 @@ func TestDeployCommand(t *testing.T) {
 			Reply(http.StatusCreated).
 			JSON(api.FunctionResponse{Id: "1"})
 		// Run test
-		assert.NoError(t, Run(context.Background(), slug, project, false, false, "", fsys))
+		noVerifyJWT := true
+		assert.NoError(t, Run(context.Background(), slug, project, &noVerifyJWT, false, "", fsys))
 		// Validate api
 		assert.Empty(t, apitest.ListUnmatchedRequests())
 	})
@@ -119,7 +123,7 @@ func TestDeployCommand(t *testing.T) {
 			Reply(http.StatusOK).
 			JSON(api.FunctionResponse{Id: "1"})
 		// Run test
-		assert.NoError(t, Run(context.Background(), slug, "", true, true, "", fsys))
+		assert.NoError(t, Run(context.Background(), slug, "", nil, true, "", fsys))
 		// Validate api
 		assert.Empty(t, apitest.ListUnmatchedRequests())
 	})
@@ -148,7 +152,7 @@ func TestDeployCommand(t *testing.T) {
 			Reply(http.StatusOK).
 			JSON(api.FunctionResponse{Id: "1"})
 		// Run test
-		assert.NoError(t, Run(context.Background(), slug, "", true, false, "", fsys))
+		assert.NoError(t, Run(context.Background(), slug, "", nil, false, "", fsys))
 		// Validate api
 		assert.Empty(t, apitest.ListUnmatchedRequests())
 	})
@@ -159,7 +163,8 @@ func TestDeployCommand(t *testing.T) {
 		// Setup invalid project ref
 		require.NoError(t, afero.WriteFile(fsys, utils.ProjectRefPath, []byte("test-project"), 0644))
 		// Run test
-		err := Run(context.Background(), "test-func", "", false, true, "", fsys)
+		noVerifyJWT := true
+		err := Run(context.Background(), "test-func", "", &noVerifyJWT, true, "", fsys)
 		// Check error
 		assert.ErrorContains(t, err, "Invalid project ref format.")
 	})
@@ -168,7 +173,8 @@ func TestDeployCommand(t *testing.T) {
 		// Setup in-memory fs
 		fsys := afero.NewMemMapFs()
 		// Run test
-		err := Run(context.Background(), "test-func", "test-project", false, true, "", fsys)
+		noVerifyJWT := true
+		err := Run(context.Background(), "test-func", "test-project", &noVerifyJWT, true, "", fsys)
 		// Check error
 		assert.ErrorContains(t, err, "Invalid project ref format.")
 	})
@@ -179,7 +185,8 @@ func TestDeployCommand(t *testing.T) {
 		// Setup valid project ref
 		project := apitest.RandomProjectRef()
 		// Run test
-		err := Run(context.Background(), "@", project, false, true, "", fsys)
+		noVerifyJWT := true
+		err := Run(context.Background(), "@", project, &noVerifyJWT, true, "", fsys)
 		// Check error
 		assert.ErrorContains(t, err, "Invalid Function name.")
 	})
@@ -190,7 +197,8 @@ func TestDeployCommand(t *testing.T) {
 		// Setup valid project ref
 		project := apitest.RandomProjectRef()
 		// Run test
-		err := Run(context.Background(), "test-func", project, false, true, "", fsys)
+		noVerifyJWT := true
+		err := Run(context.Background(), "test-func", project, &noVerifyJWT, true, "", fsys)
 		// Check error
 		assert.ErrorContains(t, err, "operation not permitted")
 	})
@@ -216,7 +224,8 @@ func TestDeployCommand(t *testing.T) {
 			Reply(http.StatusOK).
 			Body(&body)
 		// Run test
-		err = Run(context.Background(), "test-func", project, false, true, "", fsys)
+		noVerifyJWT := true
+		err = Run(context.Background(), "test-func", project, &noVerifyJWT, true, "", fsys)
 		// Check error
 		assert.ErrorContains(t, err, "Error bundling function: exit status 1\nbundle failed\n")
 		assert.Empty(t, apitest.ListUnmatchedRequests())
@@ -243,10 +252,112 @@ func TestDeployCommand(t *testing.T) {
 			Reply(http.StatusOK).
 			Body(&body)
 
-		err = Run(context.Background(), "test-func", project, false, false, "", fsys)
+		noVerifyJWT := true
+		err = Run(context.Background(), "test-func", project, &noVerifyJWT, false, "", fsys)
 		// Check error
 		assert.ErrorContains(t, err, "Error bundling function: exit status 1\neszip failed\n")
 	})
+
+	t.Run("verify_jwt param falls back to config", func(t *testing.T) {
+		const slug = "test-func"
+		// Setup in-memory fs
+		fsys := afero.NewMemMapFs()
+		require.NoError(t, utils.WriteConfig(fsys, false))
+		f, err := fsys.OpenFile("supabase/config.toml", os.O_APPEND|os.O_WRONLY, 0600)
+		require.NoError(t, err)
+		_, err = f.WriteString(`
+[functions.` + slug + `]
+verify_jwt = false
+`)
+		require.NoError(t, err)
+		require.NoError(t, f.Close())
+		// Setup valid project ref
+		project := apitest.RandomProjectRef()
+		// Setup valid access token
+		token := apitest.RandomAccessToken(t)
+		t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
+		// Setup valid deno path
+		_, err = fsys.Create(utils.DenoPathOverride)
+		require.NoError(t, err)
+		// Setup mock api
+		defer gock.OffAll()
+		gock.New("https://api.supabase.io").
+			Get("/v1/projects/" + project + "/functions/" + slug).
+			Reply(http.StatusNotFound)
+		gock.New("https://api.supabase.io").
+			Post("/v1/projects/" + project + "/functions").
+			AddMatcher(func(req *http.Request, ereq *gock.Request) (bool, error) {
+				body, err := io.ReadAll(req.Body)
+				if err != nil {
+					return false, err
+				}
+
+				var bodyJson map[string]interface{}
+				err = json.Unmarshal(body, &bodyJson)
+				if err != nil {
+					return false, err
+				}
+
+				return bodyJson["verify_jwt"] == false, nil
+			}).
+			Reply(http.StatusCreated).
+			JSON(api.FunctionResponse{Id: "1"})
+		// Run test
+		assert.NoError(t, Run(context.Background(), slug, project, nil, true, "", fsys))
+		// Validate api
+		assert.Empty(t, apitest.ListUnmatchedRequests())
+	})
+
+	t.Run("verify_jwt flag overrides config", func(t *testing.T) {
+		const slug = "test-func"
+		// Setup in-memory fs
+		fsys := afero.NewMemMapFs()
+		require.NoError(t, utils.WriteConfig(fsys, false))
+		f, err := fsys.OpenFile("supabase/config.toml", os.O_APPEND|os.O_WRONLY, 0600)
+		require.NoError(t, err)
+		_, err = f.WriteString(`
+[functions.` + slug + `]
+verify_jwt = false
+`)
+		require.NoError(t, err)
+		require.NoError(t, f.Close())
+		// Setup valid project ref
+		project := apitest.RandomProjectRef()
+		// Setup valid access token
+		token := apitest.RandomAccessToken(t)
+		t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
+		// Setup valid deno path
+		_, err = fsys.Create(utils.DenoPathOverride)
+		require.NoError(t, err)
+		// Setup mock api
+		defer gock.OffAll()
+		gock.New("https://api.supabase.io").
+			Get("/v1/projects/" + project + "/functions/" + slug).
+			Reply(http.StatusNotFound)
+		gock.New("https://api.supabase.io").
+			Post("/v1/projects/" + project + "/functions").
+			AddMatcher(func(req *http.Request, ereq *gock.Request) (bool, error) {
+				body, err := io.ReadAll(req.Body)
+				if err != nil {
+					return false, err
+				}
+
+				var bodyJson map[string]interface{}
+				err = json.Unmarshal(body, &bodyJson)
+				if err != nil {
+					return false, err
+				}
+
+				return bodyJson["verify_jwt"] == true, nil
+			}).
+			Reply(http.StatusCreated).
+			JSON(api.FunctionResponse{Id: "1"})
+		// Run test
+		noVerifyJwt := false
+		assert.NoError(t, Run(context.Background(), slug, project, &noVerifyJwt, true, "", fsys))
+		// Validate api
+		assert.Empty(t, apitest.ListUnmatchedRequests())
+	})
 }
 
 func TestDeployFunction(t *testing.T) {
diff --git a/internal/functions/serve/serve_test.go b/internal/functions/serve/serve_test.go
index 3737179e..50cba2fb 100644
--- a/internal/functions/serve/serve_test.go
+++ b/internal/functions/serve/serve_test.go
@@ -35,7 +35,7 @@ func TestServeCommand(t *testing.T) {
 			Post("/v" + utils.Docker.ClientVersion() + "/containers").
 			Reply(http.StatusServiceUnavailable)
 		// Run test
-		err := Run(context.Background(), "test-func", "", true, "", fsys)
+		err := Run(context.Background(), "test-func", "", nil, "", fsys)
 		// Check error
 		assert.ErrorContains(t, err, "request returned Service Unavailable for API route and version http://localhost/v1.41/containers/supabase_deno_relay_serve/exec")
 		assert.Empty(t, apitest.ListUnmatchedRequests())