From 95767046febe6f68f45037b76fa29880869b9ab4 Mon Sep 17 00:00:00 2001 From: Aayush Rangwala Date: Thu, 25 Jan 2024 01:56:12 +0530 Subject: [PATCH] chore: Migrate Two FA from packngo to equinix-sdk-go client (#353) Issue Task as part of migrating metal-cli from packngo to metal-go client, added the support of Two Fa to use metal-go Fixes: https://github.com/equinix/metal-cli/issues/333 Discussion: As of metal-go `0.22.2` there are 2 issues which needs api support - Accepting `otp code` in the input for `Enable and Disable 2FA` is not supported from metal-go - Receiving an `otp` on two fa registered `app` is also not supported --------- Signed-off-by: Ayush Rangwala Co-authored-by: Charles Treatman --- internal/twofa/disable2fa.go | 5 +- internal/twofa/enable2fa.go | 5 +- internal/twofa/receive.go | 9 +-- internal/twofa/twofa.go | 19 +++--- test/e2e/twofa_test.go | 118 +++++++++++++++++++++++++++++++++++ 5 files changed, 141 insertions(+), 15 deletions(-) create mode 100644 test/e2e/twofa_test.go diff --git a/internal/twofa/disable2fa.go b/internal/twofa/disable2fa.go index 29c05622..1b614cdc 100644 --- a/internal/twofa/disable2fa.go +++ b/internal/twofa/disable2fa.go @@ -21,6 +21,7 @@ package twofa import ( + "context" "fmt" "github.com/spf13/cobra" @@ -49,12 +50,12 @@ func (c *Client) Disable() *cobra.Command { cmd.SilenceUsage = true if sms { - _, err := c.Service.DisableSms(token) + _, err := c.TwoFAService.DisableTfaSms(context.Background()).XOtpToken(token).Execute() if err != nil { return fmt.Errorf("Could not disable Two-Factor Authentication via SMS: %w", err) } } else if app { - _, err := c.Service.DisableApp(token) + _, err := c.TwoFAService.DisableTfaApp(context.Background()).XOtpToken(token).Execute() if err != nil { return fmt.Errorf("Could not disable Two-Factor Authentication via App: %w", err) } diff --git a/internal/twofa/enable2fa.go b/internal/twofa/enable2fa.go index 9a9b82a3..2c6b56cc 100644 --- a/internal/twofa/enable2fa.go +++ b/internal/twofa/enable2fa.go @@ -21,6 +21,7 @@ package twofa import ( + "context" "fmt" "github.com/spf13/cobra" @@ -48,12 +49,12 @@ func (c *Client) Enable() *cobra.Command { cmd.SilenceUsage = true if sms { - _, err := c.Service.EnableSms(token) + _, err := c.TwoFAService.EnableTfaSms(context.Background()).XOtpToken(token).Execute() if err != nil { return fmt.Errorf("Could not enable Two-Factor Authentication: %w", err) } } else if app { - _, err := c.Service.EnableApp(token) + _, err := c.TwoFAService.EnableTfaApp(context.Background()).XOtpToken(token).Execute() if err != nil { return fmt.Errorf("Could not enable Two-Factor Authentication: %w", err) } diff --git a/internal/twofa/receive.go b/internal/twofa/receive.go index 64420cec..a15eef3b 100644 --- a/internal/twofa/receive.go +++ b/internal/twofa/receive.go @@ -21,6 +21,7 @@ package twofa import ( + "context" "fmt" "github.com/spf13/cobra" @@ -47,7 +48,7 @@ func (c *Client) Receive() *cobra.Command { cmd.SilenceUsage = true if sms { - _, err := c.Service.ReceiveSms() + _, err := c.OtpService.ReceiveCodes(context.Background()).Execute() if err != nil { return fmt.Errorf("Could not issue token via SMS: %w", err) } @@ -56,16 +57,16 @@ func (c *Client) Receive() *cobra.Command { return nil } - otpURI, _, err := c.Service.SeedApp() + resp, _, err := c.OtpService.SeedApp(context.Background()).Execute() if err != nil { return fmt.Errorf("Could not get the OTP Seed URI: %w", err) } data := make([][]string, 1) - data[0] = []string{otpURI} + data[0] = []string{resp.GetOtpUri()} header := []string{"OTP URI"} - return c.Out.Output(otpURI, header, &data) + return c.Out.Output(resp, header, &data) }, } diff --git a/internal/twofa/twofa.go b/internal/twofa/twofa.go index 3dcab967..d738bf46 100644 --- a/internal/twofa/twofa.go +++ b/internal/twofa/twofa.go @@ -22,14 +22,16 @@ package twofa import ( "github.com/equinix/metal-cli/internal/outputs" - "github.com/packethost/packngo" + + "github.com/equinix/equinix-sdk-go/services/metalv1" "github.com/spf13/cobra" ) type Client struct { - Servicer Servicer - Service packngo.TwoFactorAuthService - Out outputs.Outputer + Servicer Servicer + TwoFAService *metalv1.TwoFactorAuthApiService + OtpService *metalv1.OTPsApiService + Out outputs.Outputer } func (c *Client) NewCommand() *cobra.Command { @@ -45,7 +47,8 @@ func (c *Client) NewCommand() *cobra.Command { root.PersistentPreRun(cmd, args) } } - c.Service = c.Servicer.API(cmd).TwoFactorAuth + c.TwoFAService = c.Servicer.MetalAPI(cmd).TwoFactorAuthApi + c.OtpService = c.Servicer.MetalAPI(cmd).OTPsApi }, } @@ -58,8 +61,10 @@ func (c *Client) NewCommand() *cobra.Command { } type Servicer interface { - API(*cobra.Command) *packngo.Client - ListOptions(defaultIncludes, defaultExcludes []string) *packngo.ListOptions + MetalAPI(*cobra.Command) *metalv1.APIClient + Filters() map[string]string + Includes(defaultIncludes []string) (incl []string) + Excludes(defaultExcludes []string) (excl []string) } func NewClient(s Servicer, out outputs.Outputer) *Client { diff --git a/test/e2e/twofa_test.go b/test/e2e/twofa_test.go new file mode 100644 index 00000000..d6d76130 --- /dev/null +++ b/test/e2e/twofa_test.go @@ -0,0 +1,118 @@ +package hardwaretest + +import ( + "fmt" + "io" + "log" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + + root "github.com/equinix/metal-cli/internal/cli" + outputPkg "github.com/equinix/metal-cli/internal/outputs" + "github.com/equinix/metal-cli/internal/twofa" + "github.com/spf13/cobra" +) + +var mockOtpUri = "otpauth://totp/foo" + +func setupMock() *root.Client { + mockAPI := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var responseBody string + if r.URL.Path == "/user/otp/sms/receive" { + w.WriteHeader(http.StatusNoContent) + } else if r.URL.Path == "/user/otp/app/receive" { + w.Header().Add("Content-Type", "application/json") + responseBody = fmt.Sprintf(`{"otp_uri": "%v"}`, mockOtpUri) + + } else { + responseBody = fmt.Sprintf("no mock for endpoint %v", r.URL.Path) + w.WriteHeader(http.StatusNotImplemented) + } + _, err := w.Write([]byte(responseBody)) + if err != nil { + log.Fatalf("Failed to write mock response: %v", err) + } + })) + mockClient := root.NewClient("", mockAPI.URL, "metal") + return mockClient + +} + +func TestCli_Twofa(t *testing.T) { + subCommand := "2fa" + // Adjust this response as needed for your tests. + + rootClient := setupMock() + + type fields struct { + MainCmd *cobra.Command + Outputer outputPkg.Outputer + } + tests := []struct { + name string + fields fields + want *cobra.Command + cmdFunc func(*testing.T, *cobra.Command) + }{ + { + name: "receive sms", + fields: fields{ + MainCmd: twofa.NewClient(rootClient, outputPkg.Outputer(&outputPkg.Standard{})).NewCommand(), + Outputer: outputPkg.Outputer(&outputPkg.Standard{}), + }, + want: &cobra.Command{}, + cmdFunc: func(t *testing.T, c *cobra.Command) { + root := c.Root() + root.SetArgs([]string{subCommand, "receive", "-s"}) + rescueStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + if err := root.Execute(); err != nil { + t.Error(err) + } + w.Close() + out, _ := io.ReadAll(r) + + os.Stdout = rescueStdout + if !strings.Contains(string(out[:]), "SMS token sent to your phone") { + t.Error("expected output to include 'SMS token sent to your phone'.") + } + }, + }, + { + name: "receive app", + fields: fields{ + MainCmd: twofa.NewClient(rootClient, outputPkg.Outputer(&outputPkg.Standard{})).NewCommand(), + Outputer: outputPkg.Outputer(&outputPkg.Standard{}), + }, + want: &cobra.Command{}, + cmdFunc: func(t *testing.T, c *cobra.Command) { + root := c.Root() + root.SetArgs([]string{subCommand, "receive", "-a"}) + rescueStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + if err := root.Execute(); err != nil { + t.Error(err) + } + w.Close() + out, _ := io.ReadAll(r) + + os.Stdout = rescueStdout + if !strings.Contains(string(out[:]), mockOtpUri) { + t.Errorf("expected output to include %v", mockOtpUri) + } + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rootCmd := rootClient.NewCommand() + rootCmd.AddCommand(tt.fields.MainCmd) + tt.cmdFunc(t, tt.fields.MainCmd) + }) + } +}