Skip to content

Commit 9576704

Browse files
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: #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 <ayush.rangwala@gmail.com> Co-authored-by: Charles Treatman <ctreatman@equinix.com>
1 parent 9fd3c2a commit 9576704

5 files changed

Lines changed: 141 additions & 15 deletions

File tree

internal/twofa/disable2fa.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
package twofa
2222

2323
import (
24+
"context"
2425
"fmt"
2526

2627
"github.com/spf13/cobra"
@@ -49,12 +50,12 @@ func (c *Client) Disable() *cobra.Command {
4950

5051
cmd.SilenceUsage = true
5152
if sms {
52-
_, err := c.Service.DisableSms(token)
53+
_, err := c.TwoFAService.DisableTfaSms(context.Background()).XOtpToken(token).Execute()
5354
if err != nil {
5455
return fmt.Errorf("Could not disable Two-Factor Authentication via SMS: %w", err)
5556
}
5657
} else if app {
57-
_, err := c.Service.DisableApp(token)
58+
_, err := c.TwoFAService.DisableTfaApp(context.Background()).XOtpToken(token).Execute()
5859
if err != nil {
5960
return fmt.Errorf("Could not disable Two-Factor Authentication via App: %w", err)
6061
}

internal/twofa/enable2fa.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
package twofa
2222

2323
import (
24+
"context"
2425
"fmt"
2526

2627
"github.com/spf13/cobra"
@@ -48,12 +49,12 @@ func (c *Client) Enable() *cobra.Command {
4849

4950
cmd.SilenceUsage = true
5051
if sms {
51-
_, err := c.Service.EnableSms(token)
52+
_, err := c.TwoFAService.EnableTfaSms(context.Background()).XOtpToken(token).Execute()
5253
if err != nil {
5354
return fmt.Errorf("Could not enable Two-Factor Authentication: %w", err)
5455
}
5556
} else if app {
56-
_, err := c.Service.EnableApp(token)
57+
_, err := c.TwoFAService.EnableTfaApp(context.Background()).XOtpToken(token).Execute()
5758
if err != nil {
5859
return fmt.Errorf("Could not enable Two-Factor Authentication: %w", err)
5960
}

internal/twofa/receive.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
package twofa
2222

2323
import (
24+
"context"
2425
"fmt"
2526

2627
"github.com/spf13/cobra"
@@ -47,7 +48,7 @@ func (c *Client) Receive() *cobra.Command {
4748

4849
cmd.SilenceUsage = true
4950
if sms {
50-
_, err := c.Service.ReceiveSms()
51+
_, err := c.OtpService.ReceiveCodes(context.Background()).Execute()
5152
if err != nil {
5253
return fmt.Errorf("Could not issue token via SMS: %w", err)
5354
}
@@ -56,16 +57,16 @@ func (c *Client) Receive() *cobra.Command {
5657
return nil
5758
}
5859

59-
otpURI, _, err := c.Service.SeedApp()
60+
resp, _, err := c.OtpService.SeedApp(context.Background()).Execute()
6061
if err != nil {
6162
return fmt.Errorf("Could not get the OTP Seed URI: %w", err)
6263
}
6364

6465
data := make([][]string, 1)
6566

66-
data[0] = []string{otpURI}
67+
data[0] = []string{resp.GetOtpUri()}
6768
header := []string{"OTP URI"}
68-
return c.Out.Output(otpURI, header, &data)
69+
return c.Out.Output(resp, header, &data)
6970
},
7071
}
7172

internal/twofa/twofa.go

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,16 @@ package twofa
2222

2323
import (
2424
"github.com/equinix/metal-cli/internal/outputs"
25-
"github.com/packethost/packngo"
25+
26+
"github.com/equinix/equinix-sdk-go/services/metalv1"
2627
"github.com/spf13/cobra"
2728
)
2829

2930
type Client struct {
30-
Servicer Servicer
31-
Service packngo.TwoFactorAuthService
32-
Out outputs.Outputer
31+
Servicer Servicer
32+
TwoFAService *metalv1.TwoFactorAuthApiService
33+
OtpService *metalv1.OTPsApiService
34+
Out outputs.Outputer
3335
}
3436

3537
func (c *Client) NewCommand() *cobra.Command {
@@ -45,7 +47,8 @@ func (c *Client) NewCommand() *cobra.Command {
4547
root.PersistentPreRun(cmd, args)
4648
}
4749
}
48-
c.Service = c.Servicer.API(cmd).TwoFactorAuth
50+
c.TwoFAService = c.Servicer.MetalAPI(cmd).TwoFactorAuthApi
51+
c.OtpService = c.Servicer.MetalAPI(cmd).OTPsApi
4952
},
5053
}
5154

@@ -58,8 +61,10 @@ func (c *Client) NewCommand() *cobra.Command {
5861
}
5962

6063
type Servicer interface {
61-
API(*cobra.Command) *packngo.Client
62-
ListOptions(defaultIncludes, defaultExcludes []string) *packngo.ListOptions
64+
MetalAPI(*cobra.Command) *metalv1.APIClient
65+
Filters() map[string]string
66+
Includes(defaultIncludes []string) (incl []string)
67+
Excludes(defaultExcludes []string) (excl []string)
6368
}
6469

6570
func NewClient(s Servicer, out outputs.Outputer) *Client {

test/e2e/twofa_test.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package hardwaretest
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"log"
7+
"net/http"
8+
"net/http/httptest"
9+
"os"
10+
"strings"
11+
"testing"
12+
13+
root "github.com/equinix/metal-cli/internal/cli"
14+
outputPkg "github.com/equinix/metal-cli/internal/outputs"
15+
"github.com/equinix/metal-cli/internal/twofa"
16+
"github.com/spf13/cobra"
17+
)
18+
19+
var mockOtpUri = "otpauth://totp/foo"
20+
21+
func setupMock() *root.Client {
22+
mockAPI := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
23+
var responseBody string
24+
if r.URL.Path == "/user/otp/sms/receive" {
25+
w.WriteHeader(http.StatusNoContent)
26+
} else if r.URL.Path == "/user/otp/app/receive" {
27+
w.Header().Add("Content-Type", "application/json")
28+
responseBody = fmt.Sprintf(`{"otp_uri": "%v"}`, mockOtpUri)
29+
30+
} else {
31+
responseBody = fmt.Sprintf("no mock for endpoint %v", r.URL.Path)
32+
w.WriteHeader(http.StatusNotImplemented)
33+
}
34+
_, err := w.Write([]byte(responseBody))
35+
if err != nil {
36+
log.Fatalf("Failed to write mock response: %v", err)
37+
}
38+
}))
39+
mockClient := root.NewClient("", mockAPI.URL, "metal")
40+
return mockClient
41+
42+
}
43+
44+
func TestCli_Twofa(t *testing.T) {
45+
subCommand := "2fa"
46+
// Adjust this response as needed for your tests.
47+
48+
rootClient := setupMock()
49+
50+
type fields struct {
51+
MainCmd *cobra.Command
52+
Outputer outputPkg.Outputer
53+
}
54+
tests := []struct {
55+
name string
56+
fields fields
57+
want *cobra.Command
58+
cmdFunc func(*testing.T, *cobra.Command)
59+
}{
60+
{
61+
name: "receive sms",
62+
fields: fields{
63+
MainCmd: twofa.NewClient(rootClient, outputPkg.Outputer(&outputPkg.Standard{})).NewCommand(),
64+
Outputer: outputPkg.Outputer(&outputPkg.Standard{}),
65+
},
66+
want: &cobra.Command{},
67+
cmdFunc: func(t *testing.T, c *cobra.Command) {
68+
root := c.Root()
69+
root.SetArgs([]string{subCommand, "receive", "-s"})
70+
rescueStdout := os.Stdout
71+
r, w, _ := os.Pipe()
72+
os.Stdout = w
73+
if err := root.Execute(); err != nil {
74+
t.Error(err)
75+
}
76+
w.Close()
77+
out, _ := io.ReadAll(r)
78+
79+
os.Stdout = rescueStdout
80+
if !strings.Contains(string(out[:]), "SMS token sent to your phone") {
81+
t.Error("expected output to include 'SMS token sent to your phone'.")
82+
}
83+
},
84+
},
85+
{
86+
name: "receive app",
87+
fields: fields{
88+
MainCmd: twofa.NewClient(rootClient, outputPkg.Outputer(&outputPkg.Standard{})).NewCommand(),
89+
Outputer: outputPkg.Outputer(&outputPkg.Standard{}),
90+
},
91+
want: &cobra.Command{},
92+
cmdFunc: func(t *testing.T, c *cobra.Command) {
93+
root := c.Root()
94+
root.SetArgs([]string{subCommand, "receive", "-a"})
95+
rescueStdout := os.Stdout
96+
r, w, _ := os.Pipe()
97+
os.Stdout = w
98+
if err := root.Execute(); err != nil {
99+
t.Error(err)
100+
}
101+
w.Close()
102+
out, _ := io.ReadAll(r)
103+
104+
os.Stdout = rescueStdout
105+
if !strings.Contains(string(out[:]), mockOtpUri) {
106+
t.Errorf("expected output to include %v", mockOtpUri)
107+
}
108+
},
109+
},
110+
}
111+
for _, tt := range tests {
112+
t.Run(tt.name, func(t *testing.T) {
113+
rootCmd := rootClient.NewCommand()
114+
rootCmd.AddCommand(tt.fields.MainCmd)
115+
tt.cmdFunc(t, tt.fields.MainCmd)
116+
})
117+
}
118+
}

0 commit comments

Comments
 (0)