Skip to content

Commit

Permalink
chore: Migrate Two FA from packngo to equinix-sdk-go client (#353)
Browse files Browse the repository at this point in the history
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: #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 <[email protected]>
Co-authored-by: Charles Treatman <[email protected]>
  • Loading branch information
aayushrangwala and ctreatma authored Jan 24, 2024
1 parent 9fd3c2a commit 9576704
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 15 deletions.
5 changes: 3 additions & 2 deletions internal/twofa/disable2fa.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
package twofa

import (
"context"
"fmt"

"github.com/spf13/cobra"
Expand Down Expand Up @@ -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)
}
Expand Down
5 changes: 3 additions & 2 deletions internal/twofa/enable2fa.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
package twofa

import (
"context"
"fmt"

"github.com/spf13/cobra"
Expand Down Expand Up @@ -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)
}
Expand Down
9 changes: 5 additions & 4 deletions internal/twofa/receive.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
package twofa

import (
"context"
"fmt"

"github.com/spf13/cobra"
Expand All @@ -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)
}
Expand All @@ -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)
},
}

Expand Down
19 changes: 12 additions & 7 deletions internal/twofa/twofa.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
},
}

Expand All @@ -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 {
Expand Down
118 changes: 118 additions & 0 deletions test/e2e/twofa_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
}

0 comments on commit 9576704

Please sign in to comment.