Skip to content

Commit fb2e597

Browse files
authored
detect if we're using compatible tokens for MPG commands (#4534)
* detect if we're using compatible tokens for MPG commands
1 parent daa69ea commit fb2e597

File tree

9 files changed

+210
-0
lines changed

9 files changed

+210
-0
lines changed

internal/command/mpg/attach.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ func newAttach() *cobra.Command {
4343
}
4444

4545
func runAttach(ctx context.Context) error {
46+
// Check token compatibility early
47+
if err := validateMPGTokenCompatibility(ctx); err != nil {
48+
return err
49+
}
50+
4651
var (
4752
clusterId = flag.FirstArg(ctx)
4853
appName = appconfig.NameFromContext(ctx)

internal/command/mpg/connect.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ func newConnect() (cmd *cobra.Command) {
3333
}
3434

3535
func runConnect(ctx context.Context) (err error) {
36+
// Check token compatibility early
37+
if err := validateMPGTokenCompatibility(ctx); err != nil {
38+
return err
39+
}
40+
3641
io := iostreams.FromContext(ctx)
3742

3843
localProxyPort := "16380"

internal/command/mpg/create.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ func newCreate() *cobra.Command {
6969
}
7070

7171
func runCreate(ctx context.Context) error {
72+
// Check token compatibility early
73+
if err := validateMPGTokenCompatibility(ctx); err != nil {
74+
return err
75+
}
76+
7277
var (
7378
io = iostreams.FromContext(ctx)
7479
appName = flag.GetString(ctx, "name")

internal/command/mpg/destroy.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ This action is not reversible.`
3636
}
3737

3838
func runDestroy(ctx context.Context) error {
39+
// Check token compatibility early
40+
if err := validateMPGTokenCompatibility(ctx); err != nil {
41+
return err
42+
}
43+
3944
var (
4045
clusterId = flag.FirstArg(ctx)
4146
uiexClient = uiexutil.ClientFromContext(ctx)

internal/command/mpg/list.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ If no organization is specified, the user's personal organization is used.`
4040
}
4141

4242
func runList(ctx context.Context) error {
43+
// Check token compatibility early
44+
if err := validateMPGTokenCompatibility(ctx); err != nil {
45+
return err
46+
}
47+
4348
cfg := config.FromContext(ctx)
4449
out := iostreams.FromContext(ctx).Out
4550

internal/command/mpg/mpg.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
fly "github.com/superfly/fly-go"
99
"github.com/superfly/flyctl/gql"
1010
"github.com/superfly/flyctl/internal/command"
11+
"github.com/superfly/flyctl/internal/config"
1112
"github.com/superfly/flyctl/internal/flag"
1213
"github.com/superfly/flyctl/internal/flyutil"
1314
"github.com/superfly/flyctl/internal/prompt"
@@ -277,3 +278,30 @@ func ResolveOrganizationSlug(ctx context.Context, inputSlug string) (string, err
277278
// Return the canonical slug from the API response
278279
return resp.Organization.RawSlug, nil
279280
}
281+
282+
// detectTokenHasMacaroon determines if the current context has macaroon-style tokens.
283+
// MPG commands require macaroon tokens to function properly.
284+
func detectTokenHasMacaroon(ctx context.Context) bool {
285+
tokens := config.Tokens(ctx)
286+
if tokens == nil {
287+
return false
288+
}
289+
290+
// Check for macaroon tokens (newer style)
291+
return len(tokens.GetMacaroonTokens()) > 0
292+
}
293+
294+
// validateMPGTokenCompatibility checks if the current authentication tokens are compatible
295+
// with MPG commands. MPG requires macaroon-style tokens and cannot work with older bearer tokens.
296+
// Returns an error if bearer tokens are detected, suggesting the user upgrade their tokens.
297+
func validateMPGTokenCompatibility(ctx context.Context) error {
298+
if !detectTokenHasMacaroon(ctx) {
299+
return fmt.Errorf(`MPG commands require updated tokens but found older-style tokens.
300+
301+
Please upgrade your authentication by running:
302+
flyctl auth logout
303+
flyctl auth login
304+
`)
305+
}
306+
return nil
307+
}

internal/command/mpg/mpg_test.go

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import (
1010
"github.com/stretchr/testify/assert"
1111
"github.com/stretchr/testify/require"
1212
fly "github.com/superfly/fly-go"
13+
"github.com/superfly/fly-go/tokens"
1314
"github.com/superfly/flyctl/internal/command_context"
15+
"github.com/superfly/flyctl/internal/config"
1416
"github.com/superfly/flyctl/internal/flag/flagctx"
1517
"github.com/superfly/flyctl/internal/uiex"
1618
"github.com/superfly/flyctl/internal/uiexutil"
@@ -783,3 +785,148 @@ func TestCreateCommand_RegionValidation(t *testing.T) {
783785
require.NoError(t, err)
784786
assert.False(t, valid, "Should not find invalid region")
785787
}
788+
789+
// Test actual MPG token validation functions
790+
func TestMPGTokenValidation(t *testing.T) {
791+
t.Run("detectTokenHasMacaroon with actual contexts", func(t *testing.T) {
792+
// Test case 1: Context with no config (should handle gracefully)
793+
emptyCtx := context.Background()
794+
// This should panic or return false - let's catch the panic
795+
func() {
796+
defer func() {
797+
if r := recover(); r != nil {
798+
// Expected panic due to no config in context
799+
t.Logf("Expected panic caught: %v", r)
800+
}
801+
}()
802+
result := detectTokenHasMacaroon(emptyCtx)
803+
// If we get here without panicking, it should return false
804+
assert.False(t, result, "Should return false when config is nil")
805+
}()
806+
807+
// Test case 2: Context with nil tokens
808+
configWithNilTokens := &config.Config{
809+
Tokens: nil,
810+
}
811+
ctxWithNilTokens := config.NewContext(context.Background(), configWithNilTokens)
812+
result := detectTokenHasMacaroon(ctxWithNilTokens)
813+
assert.False(t, result, "Should return false when tokens are nil")
814+
815+
// Test case 3: Context with empty tokens (no macaroons)
816+
emptyTokens := tokens.Parse("") // Parse empty string creates empty tokens
817+
configWithEmptyTokens := &config.Config{
818+
Tokens: emptyTokens,
819+
}
820+
ctxWithEmptyTokens := config.NewContext(context.Background(), configWithEmptyTokens)
821+
result = detectTokenHasMacaroon(ctxWithEmptyTokens)
822+
assert.False(t, result, "Should return false when no macaroon tokens exist")
823+
824+
// Test case 4: Context with bearer tokens only (no macaroons)
825+
bearerTokens := tokens.Parse("some_bearer_token_here") // This won't be recognized as macaroon
826+
configWithBearerTokens := &config.Config{
827+
Tokens: bearerTokens,
828+
}
829+
ctxWithBearerTokens := config.NewContext(context.Background(), configWithBearerTokens)
830+
result = detectTokenHasMacaroon(ctxWithBearerTokens)
831+
assert.False(t, result, "Should return false when only bearer tokens exist")
832+
833+
// Test case 5: Context with macaroon tokens
834+
macaroonTokens := tokens.Parse("fm1r_test_macaroon_token,fm2_another_macaroon") // fm1r and fm2 prefixes are macaroon tokens
835+
configWithMacaroonTokens := &config.Config{
836+
Tokens: macaroonTokens,
837+
}
838+
ctxWithMacaroonTokens := config.NewContext(context.Background(), configWithMacaroonTokens)
839+
result = detectTokenHasMacaroon(ctxWithMacaroonTokens)
840+
assert.True(t, result, "Should return true when macaroon tokens exist")
841+
842+
// Test case 6: Context with mixed tokens (including macaroons)
843+
mixedTokens := tokens.Parse("bearer_token,fm1a_macaroon_token,oauth_token")
844+
configWithMixedTokens := &config.Config{
845+
Tokens: mixedTokens,
846+
}
847+
ctxWithMixedTokens := config.NewContext(context.Background(), configWithMixedTokens)
848+
result = detectTokenHasMacaroon(ctxWithMixedTokens)
849+
assert.True(t, result, "Should return true when macaroon tokens exist among mixed tokens")
850+
})
851+
852+
t.Run("validateMPGTokenCompatibility with actual contexts", func(t *testing.T) {
853+
// Test case 1: Context with nil tokens - should fail
854+
configWithNilTokens := &config.Config{
855+
Tokens: nil,
856+
}
857+
ctxWithNilTokens := config.NewContext(context.Background(), configWithNilTokens)
858+
err := validateMPGTokenCompatibility(ctxWithNilTokens)
859+
assert.Error(t, err, "Should return error when no macaroon tokens")
860+
assert.Contains(t, err.Error(), "MPG commands require updated tokens")
861+
assert.Contains(t, err.Error(), "flyctl auth logout")
862+
assert.Contains(t, err.Error(), "flyctl auth login")
863+
864+
// Test case 2: Context with empty tokens - should fail
865+
emptyTokens := tokens.Parse("")
866+
configWithEmptyTokens := &config.Config{
867+
Tokens: emptyTokens,
868+
}
869+
ctxWithEmptyTokens := config.NewContext(context.Background(), configWithEmptyTokens)
870+
err = validateMPGTokenCompatibility(ctxWithEmptyTokens)
871+
assert.Error(t, err, "Should return error when no macaroon tokens")
872+
assert.Contains(t, err.Error(), "MPG commands require updated tokens")
873+
874+
// Test case 3: Context with bearer tokens only - should fail
875+
bearerTokens := tokens.Parse("some_bearer_token")
876+
configWithBearerTokens := &config.Config{
877+
Tokens: bearerTokens,
878+
}
879+
ctxWithBearerTokens := config.NewContext(context.Background(), configWithBearerTokens)
880+
err = validateMPGTokenCompatibility(ctxWithBearerTokens)
881+
assert.Error(t, err, "Should return error when no macaroon tokens")
882+
assert.Contains(t, err.Error(), "MPG commands require updated tokens")
883+
884+
// Test case 4: Context with macaroon tokens - should pass
885+
macaroonTokens := tokens.Parse("fm1r_test_macaroon_token")
886+
configWithMacaroonTokens := &config.Config{
887+
Tokens: macaroonTokens,
888+
}
889+
ctxWithMacaroonTokens := config.NewContext(context.Background(), configWithMacaroonTokens)
890+
err = validateMPGTokenCompatibility(ctxWithMacaroonTokens)
891+
assert.NoError(t, err, "Should not return error when macaroon tokens exist")
892+
893+
// Test case 5: Context with mixed tokens including macaroons - should pass
894+
mixedTokens := tokens.Parse("bearer_token,fm1a_macaroon_token,oauth_token")
895+
configWithMixedTokens := &config.Config{
896+
Tokens: mixedTokens,
897+
}
898+
ctxWithMixedTokens := config.NewContext(context.Background(), configWithMixedTokens)
899+
err = validateMPGTokenCompatibility(ctxWithMixedTokens)
900+
assert.NoError(t, err, "Should not return error when macaroon tokens exist among mixed tokens")
901+
})
902+
903+
t.Run("MPG commands reject non-macaroon tokens", func(t *testing.T) {
904+
// This test verifies that actual MPG command functions call the validation
905+
// and properly reject contexts without macaroon tokens
906+
907+
// Create a context with bearer tokens only (no macaroons)
908+
bearerTokens := tokens.Parse("some_bearer_token")
909+
configWithBearerTokens := &config.Config{
910+
Tokens: bearerTokens,
911+
}
912+
ctxWithBearerTokens := config.NewContext(context.Background(), configWithBearerTokens)
913+
914+
// Test that the actual run functions would reject this context
915+
// We can't easily test the full run functions due to their dependencies,
916+
// but we can verify the validation function they call would fail
917+
err := validateMPGTokenCompatibility(ctxWithBearerTokens)
918+
assert.Error(t, err, "MPG commands should reject contexts with only bearer tokens")
919+
assert.Contains(t, err.Error(), "MPG commands require updated tokens")
920+
921+
// Create a context with macaroon tokens
922+
macaroonTokens := tokens.Parse("fm1r_macaroon_token")
923+
configWithMacaroonTokens := &config.Config{
924+
Tokens: macaroonTokens,
925+
}
926+
ctxWithMacaroonTokens := config.NewContext(context.Background(), configWithMacaroonTokens)
927+
928+
// Test that the validation would pass for macaroon tokens
929+
err = validateMPGTokenCompatibility(ctxWithMacaroonTokens)
930+
assert.NoError(t, err, "MPG commands should accept contexts with macaroon tokens")
931+
})
932+
}

internal/command/mpg/proxy.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ func newProxy() (cmd *cobra.Command) {
4343
}
4444

4545
func runProxy(ctx context.Context) (err error) {
46+
// Check token compatibility early
47+
if err := validateMPGTokenCompatibility(ctx); err != nil {
48+
return err
49+
}
50+
4651
localProxyPort := "16380"
4752
_, params, _, err := getMpgProxyParams(ctx, localProxyPort)
4853
if err != nil {

internal/command/mpg/status.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ func newStatus() *cobra.Command {
3535
}
3636

3737
func runStatus(ctx context.Context) error {
38+
// Check token compatibility early
39+
if err := validateMPGTokenCompatibility(ctx); err != nil {
40+
return err
41+
}
42+
3843
cfg := config.FromContext(ctx)
3944
out := iostreams.FromContext(ctx).Out
4045
uiexClient := uiexutil.ClientFromContext(ctx)

0 commit comments

Comments
 (0)