From bc0946ff8249eb0262cd3355bd7900551f4e57e1 Mon Sep 17 00:00:00 2001 From: RW Date: Sun, 30 Nov 2025 12:20:51 +0100 Subject: [PATCH 1/2] Add extractor migrations for JWT and PASETO --- cmd/internal/migrations/lists.go | 2 + cmd/internal/migrations/v3/common.go | 44 ++++++- cmd/internal/migrations/v3/jwt_extractor.go | 103 +++++++++++++++ .../migrations/v3/jwt_extractor_test.go | 71 ++++++++++ .../migrations/v3/paseto_extractor.go | 124 ++++++++++++++++++ .../migrations/v3/paseto_extractor_test.go | 87 ++++++++++++ 6 files changed, 428 insertions(+), 3 deletions(-) create mode 100644 cmd/internal/migrations/v3/jwt_extractor.go create mode 100644 cmd/internal/migrations/v3/jwt_extractor_test.go create mode 100644 cmd/internal/migrations/v3/paseto_extractor.go create mode 100644 cmd/internal/migrations/v3/paseto_extractor_test.go diff --git a/cmd/internal/migrations/lists.go b/cmd/internal/migrations/lists.go index 864a77e..ae536fd 100644 --- a/cmd/internal/migrations/lists.go +++ b/cmd/internal/migrations/lists.go @@ -70,6 +70,8 @@ var Migrations = []Migration{ v3migrations.MigrateSessionExtractor, v3migrations.MigrateSessionStore, v3migrations.MigrateKeyAuthConfig, + v3migrations.MigrateJWTExtractor, + v3migrations.MigratePasetoExtractor, v3migrations.MigrateTimeoutConfig, v3migrations.MigrateBasicauthAuthorizer, v3migrations.MigrateBasicauthConfig, diff --git a/cmd/internal/migrations/v3/common.go b/cmd/internal/migrations/v3/common.go index 7a99f81..24668c6 100644 --- a/cmd/internal/migrations/v3/common.go +++ b/cmd/internal/migrations/v3/common.go @@ -152,7 +152,11 @@ func removeConfigField(src, field string) string { // fn with the parsed components. If fn returns an empty string, the field is // removed entirely. func replaceKeyLookup(src string, fn func(indent, val, comma, comment, newline string) string) string { - re := regexp.MustCompile(`(?m)(\s*)KeyLookup:\s*([^\n]+)(\n?)`) + return replaceStringField(src, "KeyLookup", fn) +} + +func replaceStringField(src, field string, fn func(indent, val, comma, comment, newline string) string) string { + re := regexp.MustCompile(`(?m)^(\s*)` + regexp.QuoteMeta(field) + `:\s*([^\n]+)(\n?)`) return re.ReplaceAllStringFunc(src, func(s string) string { sub := re.FindStringSubmatch(s) indent := sub[1] @@ -187,9 +191,43 @@ func replaceKeyLookup(src string, fn func(indent, val, comma, comment, newline s } if comment != "" { - return fmt.Sprintf("%s// TODO: migrate KeyLookup: %s %s%s", indent, val, comment, newline) + return fmt.Sprintf("%s// TODO: migrate %s: %s %s%s", indent, field, val, comment, newline) + } + return fmt.Sprintf("%s// TODO: migrate %s: %s%s", indent, field, val, newline) + }) +} + +func replaceField(src, field string, fn func(indent, val, comma, comment, newline string) string) string { + re := regexp.MustCompile(`(?m)^(\s*)` + regexp.QuoteMeta(field) + `:\s*([^\n]+)(\n?)`) + return re.ReplaceAllStringFunc(src, func(s string) string { + sub := re.FindStringSubmatch(s) + indent := sub[1] + val := strings.TrimSpace(sub[2]) + newline := sub[3] + + comment := "" + if idx := strings.Index(val, "//"); idx >= 0 { + comment = strings.TrimSpace(val[idx:]) + val = strings.TrimSpace(val[:idx]) + } else if idx := strings.Index(val, "/*"); idx >= 0 { + comment = strings.TrimSpace(val[idx:]) + val = strings.TrimSpace(val[:idx]) + } + + comma := "" + if strings.HasSuffix(val, ",") { + comma = "," + val = strings.TrimSpace(strings.TrimSuffix(val, ",")) + } + + repl := fn(indent, val, comma, comment, newline) + if repl == "" { + if comment != "" { + return fmt.Sprintf("%s%s%s", indent, comment, newline) + } + return newline } - return fmt.Sprintf("%s// TODO: migrate KeyLookup: %s%s", indent, val, newline) + return repl }) } diff --git a/cmd/internal/migrations/v3/jwt_extractor.go b/cmd/internal/migrations/v3/jwt_extractor.go new file mode 100644 index 0000000..1263d9b --- /dev/null +++ b/cmd/internal/migrations/v3/jwt_extractor.go @@ -0,0 +1,103 @@ +package v3 + +import ( + "fmt" + "regexp" + "strconv" + "strings" + + semver "github.com/Masterminds/semver/v3" + "github.com/spf13/cobra" + + "github.com/gofiber/cli/cmd/internal" +) + +func MigrateJWTExtractor(cmd *cobra.Command, cwd string, _, _ *semver.Version) error { + reConfig := regexp.MustCompile(`jwtware\.Config{(?:[^{}]|{[^{}]*})*}`) + reAuthScheme := regexp.MustCompile(`(?m)^\s*AuthScheme:\s*([^,\n]+)`) + reAuthLine := regexp.MustCompile(`(?m)^\s*AuthScheme:\s*[^\n]+\n?`) + reFilter := regexp.MustCompile(`(?m)^(\s*)Filter:\s*`) + + changed, err := internal.ChangeFileContent(cwd, func(content string) string { + updated := reConfig.ReplaceAllStringFunc(content, func(cfg string) string { + schemeArg := "\"Bearer\"" + if am := reAuthScheme.FindStringSubmatch(cfg); len(am) > 1 { + raw := strings.TrimSpace(am[1]) + if uq, err := strconv.Unquote(raw); err == nil { + schemeArg = fmt.Sprintf("%q", uq) + } else { + schemeArg = raw + } + } + + cfg = replaceStringField(cfg, "TokenLookup", func(indent, val, comma, comment, newline string) string { + parts := strings.Split(val, ",") + var extractors []string + for _, p := range parts { + p = strings.TrimSpace(p) + switch { + case strings.HasPrefix(p, "header:"): + header := strings.TrimPrefix(p, "header:") + if strings.EqualFold(header, "Authorization") { + extractors = append(extractors, fmt.Sprintf("extractors.FromAuthHeader(%s)", schemeArg)) + } else { + extractors = append(extractors, fmt.Sprintf("extractors.FromHeader(%q)", header)) + } + case strings.HasPrefix(p, "query:"): + extractors = append(extractors, fmt.Sprintf("extractors.FromQuery(%q)", strings.TrimPrefix(p, "query:"))) + case strings.HasPrefix(p, "param:"): + extractors = append(extractors, fmt.Sprintf("extractors.FromParam(%q)", strings.TrimPrefix(p, "param:"))) + case strings.HasPrefix(p, "cookie:"): + extractors = append(extractors, fmt.Sprintf("extractors.FromCookie(%q)", strings.TrimPrefix(p, "cookie:"))) + case strings.HasPrefix(p, "form:"): + extractors = append(extractors, fmt.Sprintf("extractors.FromForm(%q)", strings.TrimPrefix(p, "form:"))) + default: + if comment != "" { + comment = " " + comment + } + return fmt.Sprintf("%s// TODO: migrate TokenLookup: %s%s%s", indent, val, comment, newline) + } + } + + extractor := "" + switch len(extractors) { + case 1: + extractor = extractors[0] + case 0: + default: + extractor = fmt.Sprintf("extractors.Chain(%s)", strings.Join(extractors, ", ")) + } + + if extractor == "" { + if comment != "" { + comment = " " + comment + } + return fmt.Sprintf("%s// TODO: migrate TokenLookup: %s%s%s", indent, val, comment, newline) + } + + if comment != "" { + comment = " " + comment + } + return fmt.Sprintf("%sExtractor: %s%s%s%s", indent, extractor, comma, comment, newline) + }) + + cfg = reAuthLine.ReplaceAllString(cfg, "") + cfg = reFilter.ReplaceAllString(cfg, "${1}Next: ") + return cfg + }) + + if updated != content && strings.Contains(updated, "extractors.") { + updated = addImport(updated, "github.com/gofiber/fiber/v3/extractors") + } + return updated + }) + if err != nil { + return fmt.Errorf("failed to migrate jwt extractor config: %w", err) + } + if !changed { + return nil + } + + cmd.Println("Migrating jwt middleware configs") + return nil +} diff --git a/cmd/internal/migrations/v3/jwt_extractor_test.go b/cmd/internal/migrations/v3/jwt_extractor_test.go new file mode 100644 index 0000000..44bdce0 --- /dev/null +++ b/cmd/internal/migrations/v3/jwt_extractor_test.go @@ -0,0 +1,71 @@ +package v3_test + +import ( + "bytes" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/gofiber/cli/cmd/internal/migrations/v3" +) + +func Test_MigrateJWTExtractor(t *testing.T) { + t.Parallel() + + dir, err := os.MkdirTemp("", "mjwt") + require.NoError(t, err) + defer func() { require.NoError(t, os.RemoveAll(dir)) }() + + file := writeTempFile(t, dir, `package main +import ( + "github.com/gofiber/fiber/v3" + jwtware "github.com/gofiber/contrib/jwt" +) +var _ = jwtware.New(jwtware.Config{ + TokenLookup: "header:Authorization, cookie:jwt", + AuthScheme: "Token", + Filter: func(c fiber.Ctx) bool { return false }, +})`) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, v3.MigrateJWTExtractor(cmd, dir, nil, nil)) + + content := readFile(t, file) + assert.NotContains(t, content, "TokenLookup") + assert.NotContains(t, content, "AuthScheme") + assert.Contains(t, content, `Extractor: extractors.Chain(extractors.FromAuthHeader("Token"), extractors.FromCookie("jwt"))`) + assert.Contains(t, content, "Next:") + assert.Contains(t, content, "func(c fiber.Ctx) bool { return false },") + assert.Contains(t, content, `"github.com/gofiber/fiber/v3/extractors"`) + assert.Contains(t, buf.String(), "Migrating jwt middleware configs") +} + +func Test_MigrateJWTExtractor_TokenLookupExpr(t *testing.T) { + t.Parallel() + + dir, err := os.MkdirTemp("", "mjwt_expr") + require.NoError(t, err) + defer func() { require.NoError(t, os.RemoveAll(dir)) }() + + file := writeTempFile(t, dir, `package main +import ( + jwtware "github.com/gofiber/contrib/jwt" + "strings" +) +var _ = jwtware.New(jwtware.Config{ + TokenLookup: strings.Join([]string{"header:Authorization"}, ","), + AuthScheme: "Bearer", +})`) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, v3.MigrateJWTExtractor(cmd, dir, nil, nil)) + + content := readFile(t, file) + assert.Contains(t, content, "// TODO: migrate TokenLookup: strings.Join([]string{\"header:Authorization\"}, \",\")") + assert.NotContains(t, content, "AuthScheme") + assert.Contains(t, buf.String(), "Migrating jwt middleware configs") +} diff --git a/cmd/internal/migrations/v3/paseto_extractor.go b/cmd/internal/migrations/v3/paseto_extractor.go new file mode 100644 index 0000000..706c205 --- /dev/null +++ b/cmd/internal/migrations/v3/paseto_extractor.go @@ -0,0 +1,124 @@ +package v3 + +import ( + "fmt" + "regexp" + "strconv" + "strings" + + semver "github.com/Masterminds/semver/v3" + "github.com/spf13/cobra" + + "github.com/gofiber/cli/cmd/internal" +) + +func MigratePasetoExtractor(cmd *cobra.Command, cwd string, _, _ *semver.Version) error { + reConfig := regexp.MustCompile(`pasetoware\.Config{(?:[^{}]|{[^{}]*})*}`) + reTokenPrefix := regexp.MustCompile(`(?m)\s*TokenPrefix:\s*([^,\n]+)`) + + changed, err := internal.ChangeFileContent(cwd, func(content string) string { + updated := reConfig.ReplaceAllStringFunc(content, func(cfg string) string { + schemeArg := "" + hasPrefix := false + if am := reTokenPrefix.FindStringSubmatch(cfg); len(am) > 1 { + hasPrefix = true + raw := strings.TrimSpace(am[1]) + if uq, err := strconv.Unquote(raw); err == nil { + schemeArg = fmt.Sprintf("%q", uq) + } else { + schemeArg = raw + } + } + + cfg = replaceField(cfg, "TokenLookup", func(indent, val, comma, comment, newline string) string { + lookup := strings.TrimSpace(val) + lookup = strings.TrimPrefix(lookup, "[2]string") + lookup = strings.TrimSpace(strings.TrimPrefix(lookup, "{")) + lookup = strings.TrimSuffix(lookup, "}") + parts := splitArgs(lookup) + if len(parts) < 2 { + if comment != "" { + comment = " " + comment + } + return fmt.Sprintf("%s// TODO: migrate TokenLookup: %s%s%s", indent, val, comment, newline) + } + + source := strings.TrimSpace(parts[0]) + key := strings.TrimSpace(parts[1]) + + sourceLower := strings.ToLower(source) + if uq, err := strconv.Unquote(sourceLower); err == nil { + sourceLower = strings.ToLower(uq) + } + switch { + case strings.Contains(sourceLower, "lookupheader"): + sourceLower = "header" + case strings.Contains(sourceLower, "lookupquery"): + sourceLower = "query" + case strings.Contains(sourceLower, "lookupparam"): + sourceLower = "param" + case strings.Contains(sourceLower, "lookupcookie"): + sourceLower = "cookie" + case strings.Contains(sourceLower, "lookupform"): + sourceLower = "form" + default: + // preserve original value for unsupported lookups + } + + keyArg := key + if uq, err := strconv.Unquote(keyArg); err == nil { + keyArg = fmt.Sprintf("%q", uq) + } + + var extractor string + switch sourceLower { + case "header": + if strings.EqualFold(strings.Trim(keyArg, "\""), "Authorization") && hasPrefix && strings.Trim(keyArg, "\"") != "" { + extractor = fmt.Sprintf("extractors.FromAuthHeader(%s)", schemeArg) + } else { + extractor = fmt.Sprintf("extractors.FromHeader(%s)", keyArg) + } + case "query": + extractor = fmt.Sprintf("extractors.FromQuery(%s)", keyArg) + case "param": + extractor = fmt.Sprintf("extractors.FromParam(%s)", keyArg) + case "cookie": + extractor = fmt.Sprintf("extractors.FromCookie(%s)", keyArg) + case "form": + extractor = fmt.Sprintf("extractors.FromForm(%s)", keyArg) + default: + // leave extractor empty to emit TODO comment below + } + + if extractor == "" { + if comment != "" { + comment = " " + comment + } + return fmt.Sprintf("%s// TODO: migrate TokenLookup: %s%s%s", indent, val, comment, newline) + } + + if comment != "" { + comment = " " + comment + } + return fmt.Sprintf("%sExtractor: %s%s%s%s", indent, extractor, comma, comment, newline) + }) + + cfg = removeConfigField(cfg, "TokenPrefix") + return cfg + }) + + if updated != content && strings.Contains(updated, "extractors.") { + updated = addImport(updated, "github.com/gofiber/fiber/v3/extractors") + } + return updated + }) + if err != nil { + return fmt.Errorf("failed to migrate paseto extractor config: %w", err) + } + if !changed { + return nil + } + + cmd.Println("Migrating paseto middleware configs") + return nil +} diff --git a/cmd/internal/migrations/v3/paseto_extractor_test.go b/cmd/internal/migrations/v3/paseto_extractor_test.go new file mode 100644 index 0000000..0d725e3 --- /dev/null +++ b/cmd/internal/migrations/v3/paseto_extractor_test.go @@ -0,0 +1,87 @@ +package v3_test + +import ( + "bytes" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/gofiber/cli/cmd/internal/migrations/v3" +) + +func Test_MigratePasetoExtractor_HeaderPrefix(t *testing.T) { + t.Parallel() + + dir, err := os.MkdirTemp("", "mpaseto") + require.NoError(t, err) + defer func() { require.NoError(t, os.RemoveAll(dir)) }() + + file := writeTempFile(t, dir, `package main +import pasetoware "github.com/gofiber/contrib/paseto" +var _ = pasetoware.New(pasetoware.Config{ + TokenLookup: [2]string{"header", "Authorization"}, + TokenPrefix: "Bearer", +})`) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, v3.MigratePasetoExtractor(cmd, dir, nil, nil)) + + content := readFile(t, file) + assert.NotContains(t, content, "TokenLookup") + assert.NotContains(t, content, "TokenPrefix") + assert.Contains(t, content, `Extractor: extractors.FromAuthHeader("Bearer")`) + assert.Contains(t, content, `"github.com/gofiber/fiber/v3/extractors"`) + assert.Contains(t, buf.String(), "Migrating paseto middleware configs") +} + +func Test_MigratePasetoExtractor_QueryNoPrefix(t *testing.T) { + t.Parallel() + + dir, err := os.MkdirTemp("", "mpaseto_query") + require.NoError(t, err) + defer func() { require.NoError(t, os.RemoveAll(dir)) }() + + file := writeTempFile(t, dir, `package main +import pasetoware "github.com/gofiber/contrib/paseto" +var _ = pasetoware.New(pasetoware.Config{ + TokenLookup: [2]string{pasetoware.LookupQuery, "token"}, +})`) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, v3.MigratePasetoExtractor(cmd, dir, nil, nil)) + + content := readFile(t, file) + assert.NotContains(t, content, "TokenLookup") + assert.Contains(t, content, `Extractor: extractors.FromQuery("token")`) + assert.Contains(t, buf.String(), "Migrating paseto middleware configs") +} + +func Test_MigratePasetoExtractor_UnsupportedLookup(t *testing.T) { + t.Parallel() + + dir, err := os.MkdirTemp("", "mpaseto_todo") + require.NoError(t, err) + defer func() { require.NoError(t, os.RemoveAll(dir)) }() + + file := writeTempFile(t, dir, `package main +import ( + pasetoware "github.com/gofiber/contrib/paseto" + "strings" +) +var _ = pasetoware.New(pasetoware.Config{ + TokenLookup: [2]string{strings.ToUpper("header"), "Authorization"}, +})`) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, v3.MigratePasetoExtractor(cmd, dir, nil, nil)) + + content := readFile(t, file) + assert.Contains(t, content, "// TODO: migrate TokenLookup: [2]string{strings.ToUpper(\"header\"), \"Authorization\"}") + assert.NotContains(t, content, "TokenPrefix") + assert.Contains(t, buf.String(), "Migrating paseto middleware configs") +} From 21b10fe1b4ea60f319916cc3b925836450715d8a Mon Sep 17 00:00:00 2001 From: RW Date: Sun, 30 Nov 2025 13:05:42 +0100 Subject: [PATCH 2/2] Handle extractor migrations with import aliases --- cmd/internal/migrations/v3/common.go | 73 ++++---- cmd/internal/migrations/v3/jwt_extractor.go | 113 ++++++------ .../migrations/v3/jwt_extractor_test.go | 47 +++++ .../migrations/v3/paseto_extractor.go | 163 +++++++++--------- .../migrations/v3/paseto_extractor_test.go | 47 +++++ 5 files changed, 277 insertions(+), 166 deletions(-) diff --git a/cmd/internal/migrations/v3/common.go b/cmd/internal/migrations/v3/common.go index 24668c6..3fb3deb 100644 --- a/cmd/internal/migrations/v3/common.go +++ b/cmd/internal/migrations/v3/common.go @@ -3,6 +3,7 @@ package v3 import ( "fmt" "regexp" + "sort" "strconv" "strings" ) @@ -156,6 +157,14 @@ func replaceKeyLookup(src string, fn func(indent, val, comma, comment, newline s } func replaceStringField(src, field string, fn func(indent, val, comma, comment, newline string) string) string { + return replaceFieldImpl(src, field, true, fn) +} + +func replaceField(src, field string, fn func(indent, val, comma, comment, newline string) string) string { + return replaceFieldImpl(src, field, false, fn) +} + +func replaceFieldImpl(src, field string, unquote bool, fn func(indent, val, comma, comment, newline string) string) string { re := regexp.MustCompile(`(?m)^(\s*)` + regexp.QuoteMeta(field) + `:\s*([^\n]+)(\n?)`) return re.ReplaceAllStringFunc(src, func(s string) string { sub := re.FindStringSubmatch(s) @@ -178,46 +187,15 @@ func replaceStringField(src, field string, fn func(indent, val, comma, comment, val = strings.TrimSpace(strings.TrimSuffix(val, ",")) } - if uq, err := strconv.Unquote(val); err == nil { - val = uq - repl := fn(indent, val, comma, comment, newline) - if repl == "" { + if unquote { + uq, err := strconv.Unquote(val) + if err != nil { if comment != "" { - return fmt.Sprintf("%s%s%s", indent, comment, newline) + return fmt.Sprintf("%s// TODO: migrate %s: %s %s%s", indent, field, val, comment, newline) } - return newline + return fmt.Sprintf("%s// TODO: migrate %s: %s%s", indent, field, val, newline) } - return repl - } - - if comment != "" { - return fmt.Sprintf("%s// TODO: migrate %s: %s %s%s", indent, field, val, comment, newline) - } - return fmt.Sprintf("%s// TODO: migrate %s: %s%s", indent, field, val, newline) - }) -} - -func replaceField(src, field string, fn func(indent, val, comma, comment, newline string) string) string { - re := regexp.MustCompile(`(?m)^(\s*)` + regexp.QuoteMeta(field) + `:\s*([^\n]+)(\n?)`) - return re.ReplaceAllStringFunc(src, func(s string) string { - sub := re.FindStringSubmatch(s) - indent := sub[1] - val := strings.TrimSpace(sub[2]) - newline := sub[3] - - comment := "" - if idx := strings.Index(val, "//"); idx >= 0 { - comment = strings.TrimSpace(val[idx:]) - val = strings.TrimSpace(val[:idx]) - } else if idx := strings.Index(val, "/*"); idx >= 0 { - comment = strings.TrimSpace(val[idx:]) - val = strings.TrimSpace(val[:idx]) - } - - comma := "" - if strings.HasSuffix(val, ",") { - comma = "," - val = strings.TrimSpace(strings.TrimSuffix(val, ",")) + val = uq } repl := fn(indent, val, comma, comment, newline) @@ -231,6 +209,27 @@ func replaceField(src, field string, fn func(indent, val, comma, comment, newlin }) } +func collectAliases(content string, reImport *regexp.Regexp, defaults []string) []string { + aliases := map[string]struct{}{} + for _, m := range reImport.FindAllStringSubmatch(content, -1) { + alias := strings.TrimSpace(m[1]) + if alias == "" { + for _, d := range defaults { + aliases[d] = struct{}{} + } + continue + } + aliases[alias] = struct{}{} + } + + result := make([]string, 0, len(aliases)) + for alias := range aliases { + result = append(result, alias) + } + sort.Strings(result) + return result +} + // splitArgs splits a comma-separated argument list into its individual arguments // while respecting nested parentheses, brackets, and braces as well as quoted // strings. It returns the trimmed arguments without altering inner spacing. diff --git a/cmd/internal/migrations/v3/jwt_extractor.go b/cmd/internal/migrations/v3/jwt_extractor.go index 1263d9b..ddea3e4 100644 --- a/cmd/internal/migrations/v3/jwt_extractor.go +++ b/cmd/internal/migrations/v3/jwt_extractor.go @@ -13,78 +13,87 @@ import ( ) func MigrateJWTExtractor(cmd *cobra.Command, cwd string, _, _ *semver.Version) error { - reConfig := regexp.MustCompile(`jwtware\.Config{(?:[^{}]|{[^{}]*})*}`) + reImport := regexp.MustCompile(`(?m)^\s*(?:import\s+)?(?:([\w\.]+)\s+)?"github\.com/gofiber/contrib/jwt(?:/v\d+)?"`) reAuthScheme := regexp.MustCompile(`(?m)^\s*AuthScheme:\s*([^,\n]+)`) reAuthLine := regexp.MustCompile(`(?m)^\s*AuthScheme:\s*[^\n]+\n?`) reFilter := regexp.MustCompile(`(?m)^(\s*)Filter:\s*`) changed, err := internal.ChangeFileContent(cwd, func(content string) string { - updated := reConfig.ReplaceAllStringFunc(content, func(cfg string) string { - schemeArg := "\"Bearer\"" - if am := reAuthScheme.FindStringSubmatch(cfg); len(am) > 1 { - raw := strings.TrimSpace(am[1]) - if uq, err := strconv.Unquote(raw); err == nil { - schemeArg = fmt.Sprintf("%q", uq) - } else { - schemeArg = raw + aliases := collectAliases(content, reImport, []string{"jwtware", "jwt"}) + if len(aliases) == 0 { + return content + } + + updated := content + for _, alias := range aliases { + reConfig := regexp.MustCompile(regexp.QuoteMeta(alias) + `\.Config{(?:[^{}]|{[^{}]*})*}`) + updated = reConfig.ReplaceAllStringFunc(updated, func(cfg string) string { + schemeArg := "\"Bearer\"" + if am := reAuthScheme.FindStringSubmatch(cfg); len(am) > 1 { + raw := strings.TrimSpace(am[1]) + if uq, err := strconv.Unquote(raw); err == nil { + schemeArg = fmt.Sprintf("%q", uq) + } else { + schemeArg = raw + } } - } - cfg = replaceStringField(cfg, "TokenLookup", func(indent, val, comma, comment, newline string) string { - parts := strings.Split(val, ",") - var extractors []string - for _, p := range parts { - p = strings.TrimSpace(p) - switch { - case strings.HasPrefix(p, "header:"): - header := strings.TrimPrefix(p, "header:") - if strings.EqualFold(header, "Authorization") { - extractors = append(extractors, fmt.Sprintf("extractors.FromAuthHeader(%s)", schemeArg)) - } else { - extractors = append(extractors, fmt.Sprintf("extractors.FromHeader(%q)", header)) + cfg = replaceStringField(cfg, "TokenLookup", func(indent, val, comma, comment, newline string) string { + parts := strings.Split(val, ",") + var extractors []string + for _, p := range parts { + p = strings.TrimSpace(p) + switch { + case strings.HasPrefix(p, "header:"): + header := strings.TrimPrefix(p, "header:") + if strings.EqualFold(header, "Authorization") { + extractors = append(extractors, fmt.Sprintf("extractors.FromAuthHeader(%s)", schemeArg)) + } else { + extractors = append(extractors, fmt.Sprintf("extractors.FromHeader(%q)", header)) + } + case strings.HasPrefix(p, "query:"): + extractors = append(extractors, fmt.Sprintf("extractors.FromQuery(%q)", strings.TrimPrefix(p, "query:"))) + case strings.HasPrefix(p, "param:"): + extractors = append(extractors, fmt.Sprintf("extractors.FromParam(%q)", strings.TrimPrefix(p, "param:"))) + case strings.HasPrefix(p, "cookie:"): + extractors = append(extractors, fmt.Sprintf("extractors.FromCookie(%q)", strings.TrimPrefix(p, "cookie:"))) + case strings.HasPrefix(p, "form:"): + extractors = append(extractors, fmt.Sprintf("extractors.FromForm(%q)", strings.TrimPrefix(p, "form:"))) + default: + if comment != "" { + comment = " " + comment + } + return fmt.Sprintf("%s// TODO: migrate TokenLookup: %s%s%s", indent, val, comment, newline) } - case strings.HasPrefix(p, "query:"): - extractors = append(extractors, fmt.Sprintf("extractors.FromQuery(%q)", strings.TrimPrefix(p, "query:"))) - case strings.HasPrefix(p, "param:"): - extractors = append(extractors, fmt.Sprintf("extractors.FromParam(%q)", strings.TrimPrefix(p, "param:"))) - case strings.HasPrefix(p, "cookie:"): - extractors = append(extractors, fmt.Sprintf("extractors.FromCookie(%q)", strings.TrimPrefix(p, "cookie:"))) - case strings.HasPrefix(p, "form:"): - extractors = append(extractors, fmt.Sprintf("extractors.FromForm(%q)", strings.TrimPrefix(p, "form:"))) + } + + extractor := "" + switch len(extractors) { + case 1: + extractor = extractors[0] + case 0: default: + extractor = fmt.Sprintf("extractors.Chain(%s)", strings.Join(extractors, ", ")) + } + + if extractor == "" { if comment != "" { comment = " " + comment } return fmt.Sprintf("%s// TODO: migrate TokenLookup: %s%s%s", indent, val, comment, newline) } - } - - extractor := "" - switch len(extractors) { - case 1: - extractor = extractors[0] - case 0: - default: - extractor = fmt.Sprintf("extractors.Chain(%s)", strings.Join(extractors, ", ")) - } - if extractor == "" { if comment != "" { comment = " " + comment } - return fmt.Sprintf("%s// TODO: migrate TokenLookup: %s%s%s", indent, val, comment, newline) - } + return fmt.Sprintf("%sExtractor: %s%s%s%s", indent, extractor, comma, comment, newline) + }) - if comment != "" { - comment = " " + comment - } - return fmt.Sprintf("%sExtractor: %s%s%s%s", indent, extractor, comma, comment, newline) + cfg = reAuthLine.ReplaceAllString(cfg, "") + cfg = reFilter.ReplaceAllString(cfg, "${1}Next: ") + return cfg }) - - cfg = reAuthLine.ReplaceAllString(cfg, "") - cfg = reFilter.ReplaceAllString(cfg, "${1}Next: ") - return cfg - }) + } if updated != content && strings.Contains(updated, "extractors.") { updated = addImport(updated, "github.com/gofiber/fiber/v3/extractors") diff --git a/cmd/internal/migrations/v3/jwt_extractor_test.go b/cmd/internal/migrations/v3/jwt_extractor_test.go index 44bdce0..c908790 100644 --- a/cmd/internal/migrations/v3/jwt_extractor_test.go +++ b/cmd/internal/migrations/v3/jwt_extractor_test.go @@ -69,3 +69,50 @@ var _ = jwtware.New(jwtware.Config{ assert.NotContains(t, content, "AuthScheme") assert.Contains(t, buf.String(), "Migrating jwt middleware configs") } + +func Test_MigrateJWTExtractor_CustomAlias(t *testing.T) { + t.Parallel() + + dir, err := os.MkdirTemp("", "mjwt_alias") + require.NoError(t, err) + defer func() { require.NoError(t, os.RemoveAll(dir)) }() + + file := writeTempFile(t, dir, `package main +import authjwt "github.com/gofiber/contrib/jwt/v3" +var _ = authjwt.New(authjwt.Config{ + TokenLookup: "cookie:session", +})`) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, v3.MigrateJWTExtractor(cmd, dir, nil, nil)) + + content := readFile(t, file) + assert.NotContains(t, content, "TokenLookup") + assert.Contains(t, content, `Extractor: extractors.FromCookie("session")`) + assert.Contains(t, content, `"github.com/gofiber/fiber/v3/extractors"`) + assert.Contains(t, buf.String(), "Migrating jwt middleware configs") +} + +func Test_MigrateJWTExtractor_SkipUnrelatedPackage(t *testing.T) { + t.Parallel() + + dir, err := os.MkdirTemp("", "mjwt_skip") + require.NoError(t, err) + defer func() { require.NoError(t, os.RemoveAll(dir)) }() + + original := `package main +import jwtware "example.com/jwtware" +var _ = jwtware.Config{ + TokenLookup: "header:Authorization", +}` + file := writeTempFile(t, dir, original) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, v3.MigrateJWTExtractor(cmd, dir, nil, nil)) + + content := readFile(t, file) + assert.Equal(t, original, content) + assert.Empty(t, buf.String()) +} diff --git a/cmd/internal/migrations/v3/paseto_extractor.go b/cmd/internal/migrations/v3/paseto_extractor.go index 706c205..d19478d 100644 --- a/cmd/internal/migrations/v3/paseto_extractor.go +++ b/cmd/internal/migrations/v3/paseto_extractor.go @@ -13,99 +13,108 @@ import ( ) func MigratePasetoExtractor(cmd *cobra.Command, cwd string, _, _ *semver.Version) error { - reConfig := regexp.MustCompile(`pasetoware\.Config{(?:[^{}]|{[^{}]*})*}`) + reImport := regexp.MustCompile(`(?m)^\s*(?:import\s+)?(?:([\w\.]+)\s+)?"github\.com/gofiber/contrib/paseto(?:/v\d+)?"`) reTokenPrefix := regexp.MustCompile(`(?m)\s*TokenPrefix:\s*([^,\n]+)`) changed, err := internal.ChangeFileContent(cwd, func(content string) string { - updated := reConfig.ReplaceAllStringFunc(content, func(cfg string) string { - schemeArg := "" - hasPrefix := false - if am := reTokenPrefix.FindStringSubmatch(cfg); len(am) > 1 { - hasPrefix = true - raw := strings.TrimSpace(am[1]) - if uq, err := strconv.Unquote(raw); err == nil { - schemeArg = fmt.Sprintf("%q", uq) - } else { - schemeArg = raw - } - } - - cfg = replaceField(cfg, "TokenLookup", func(indent, val, comma, comment, newline string) string { - lookup := strings.TrimSpace(val) - lookup = strings.TrimPrefix(lookup, "[2]string") - lookup = strings.TrimSpace(strings.TrimPrefix(lookup, "{")) - lookup = strings.TrimSuffix(lookup, "}") - parts := splitArgs(lookup) - if len(parts) < 2 { - if comment != "" { - comment = " " + comment + aliases := collectAliases(content, reImport, []string{"pasetoware", "paseto"}) + if len(aliases) == 0 { + return content + } + + updated := content + for _, alias := range aliases { + reConfig := regexp.MustCompile(regexp.QuoteMeta(alias) + `\.Config{(?:[^{}]|{[^{}]*})*}`) + updated = reConfig.ReplaceAllStringFunc(updated, func(cfg string) string { + schemeArg := "" + hasPrefix := false + if am := reTokenPrefix.FindStringSubmatch(cfg); len(am) > 1 { + hasPrefix = true + raw := strings.TrimSpace(am[1]) + if uq, err := strconv.Unquote(raw); err == nil { + schemeArg = fmt.Sprintf("%q", uq) + } else { + schemeArg = raw } - return fmt.Sprintf("%s// TODO: migrate TokenLookup: %s%s%s", indent, val, comment, newline) } - source := strings.TrimSpace(parts[0]) - key := strings.TrimSpace(parts[1]) + cfg = replaceField(cfg, "TokenLookup", func(indent, val, comma, comment, newline string) string { + lookup := strings.TrimSpace(val) + lookup = strings.TrimPrefix(lookup, "[2]string") + lookup = strings.TrimSpace(strings.TrimPrefix(lookup, "{")) + lookup = strings.TrimSuffix(lookup, "}") + parts := splitArgs(lookup) + if len(parts) < 2 { + if comment != "" { + comment = " " + comment + } + return fmt.Sprintf("%s// TODO: migrate TokenLookup: %s%s%s", indent, val, comment, newline) + } - sourceLower := strings.ToLower(source) - if uq, err := strconv.Unquote(sourceLower); err == nil { - sourceLower = strings.ToLower(uq) - } - switch { - case strings.Contains(sourceLower, "lookupheader"): - sourceLower = "header" - case strings.Contains(sourceLower, "lookupquery"): - sourceLower = "query" - case strings.Contains(sourceLower, "lookupparam"): - sourceLower = "param" - case strings.Contains(sourceLower, "lookupcookie"): - sourceLower = "cookie" - case strings.Contains(sourceLower, "lookupform"): - sourceLower = "form" - default: - // preserve original value for unsupported lookups - } + source := strings.TrimSpace(parts[0]) + key := strings.TrimSpace(parts[1]) - keyArg := key - if uq, err := strconv.Unquote(keyArg); err == nil { - keyArg = fmt.Sprintf("%q", uq) - } + sourceLower := strings.ToLower(source) + if uq, err := strconv.Unquote(sourceLower); err == nil { + sourceLower = strings.ToLower(uq) + } + switch { + case strings.Contains(sourceLower, "lookupheader"): + sourceLower = "header" + case strings.Contains(sourceLower, "lookupquery"): + sourceLower = "query" + case strings.Contains(sourceLower, "lookupparam"): + sourceLower = "param" + case strings.Contains(sourceLower, "lookupcookie"): + sourceLower = "cookie" + case strings.Contains(sourceLower, "lookupform"): + sourceLower = "form" + default: + // preserve original value for unsupported lookups + } - var extractor string - switch sourceLower { - case "header": - if strings.EqualFold(strings.Trim(keyArg, "\""), "Authorization") && hasPrefix && strings.Trim(keyArg, "\"") != "" { - extractor = fmt.Sprintf("extractors.FromAuthHeader(%s)", schemeArg) - } else { - extractor = fmt.Sprintf("extractors.FromHeader(%s)", keyArg) + keyArg := key + if uq, err := strconv.Unquote(keyArg); err == nil { + keyArg = fmt.Sprintf("%q", uq) + } + + var extractor string + switch sourceLower { + case "header": + if strings.EqualFold(strings.Trim(keyArg, "\""), "Authorization") && hasPrefix { + extractor = fmt.Sprintf("extractors.FromAuthHeader(%s)", schemeArg) + } else { + extractor = fmt.Sprintf("extractors.FromHeader(%s)", keyArg) + } + case "query": + extractor = fmt.Sprintf("extractors.FromQuery(%s)", keyArg) + case "param": + extractor = fmt.Sprintf("extractors.FromParam(%s)", keyArg) + case "cookie": + extractor = fmt.Sprintf("extractors.FromCookie(%s)", keyArg) + case "form": + extractor = fmt.Sprintf("extractors.FromForm(%s)", keyArg) + default: + // leave extractor empty to emit TODO comment below + } + + if extractor == "" { + if comment != "" { + comment = " " + comment + } + return fmt.Sprintf("%s// TODO: migrate TokenLookup: %s%s%s", indent, val, comment, newline) } - case "query": - extractor = fmt.Sprintf("extractors.FromQuery(%s)", keyArg) - case "param": - extractor = fmt.Sprintf("extractors.FromParam(%s)", keyArg) - case "cookie": - extractor = fmt.Sprintf("extractors.FromCookie(%s)", keyArg) - case "form": - extractor = fmt.Sprintf("extractors.FromForm(%s)", keyArg) - default: - // leave extractor empty to emit TODO comment below - } - if extractor == "" { if comment != "" { comment = " " + comment } - return fmt.Sprintf("%s// TODO: migrate TokenLookup: %s%s%s", indent, val, comment, newline) - } + return fmt.Sprintf("%sExtractor: %s%s%s%s", indent, extractor, comma, comment, newline) + }) - if comment != "" { - comment = " " + comment - } - return fmt.Sprintf("%sExtractor: %s%s%s%s", indent, extractor, comma, comment, newline) + cfg = removeConfigField(cfg, "TokenPrefix") + return cfg }) - - cfg = removeConfigField(cfg, "TokenPrefix") - return cfg - }) + } if updated != content && strings.Contains(updated, "extractors.") { updated = addImport(updated, "github.com/gofiber/fiber/v3/extractors") diff --git a/cmd/internal/migrations/v3/paseto_extractor_test.go b/cmd/internal/migrations/v3/paseto_extractor_test.go index 0d725e3..fc8f0ec 100644 --- a/cmd/internal/migrations/v3/paseto_extractor_test.go +++ b/cmd/internal/migrations/v3/paseto_extractor_test.go @@ -85,3 +85,50 @@ var _ = pasetoware.New(pasetoware.Config{ assert.NotContains(t, content, "TokenPrefix") assert.Contains(t, buf.String(), "Migrating paseto middleware configs") } + +func Test_MigratePasetoExtractor_CustomAlias(t *testing.T) { + t.Parallel() + + dir, err := os.MkdirTemp("", "mpaseto_alias") + require.NoError(t, err) + defer func() { require.NoError(t, os.RemoveAll(dir)) }() + + file := writeTempFile(t, dir, `package main +import authpaseto "github.com/gofiber/contrib/paseto/v3" +var _ = authpaseto.New(authpaseto.Config{ + TokenLookup: [2]string{"cookie", "session"}, +})`) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, v3.MigratePasetoExtractor(cmd, dir, nil, nil)) + + content := readFile(t, file) + assert.NotContains(t, content, "TokenLookup") + assert.Contains(t, content, `Extractor: extractors.FromCookie("session")`) + assert.Contains(t, content, `"github.com/gofiber/fiber/v3/extractors"`) + assert.Contains(t, buf.String(), "Migrating paseto middleware configs") +} + +func Test_MigratePasetoExtractor_SkipUnrelatedPackage(t *testing.T) { + t.Parallel() + + dir, err := os.MkdirTemp("", "mpaseto_skip") + require.NoError(t, err) + defer func() { require.NoError(t, os.RemoveAll(dir)) }() + + original := `package main +import pasetoware "example.com/paseto" +var _ = pasetoware.Config{ + TokenLookup: [2]string{"header", "Authorization"}, +}` + file := writeTempFile(t, dir, original) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, v3.MigratePasetoExtractor(cmd, dir, nil, nil)) + + content := readFile(t, file) + assert.Equal(t, original, content) + assert.Empty(t, buf.String()) +}