summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorIgor Drozdov <idrozdov@gitlab.com>2022-07-16 15:15:06 +0200
committerIgor Drozdov <idrozdov@gitlab.com>2022-07-20 16:24:51 +0200
commitf6feedf9008aff0713e4ff40bba3617fc724032a (patch)
treedfc4d03d95d2fb250e1d395e56b00b708d23a342 /internal
parentfe5feeea22a639a4835724cf42b337773b54d83c (diff)
downloadgitlab-shell-f6feedf9008aff0713e4ff40bba3617fc724032a.tar.gz
Simplify 2FA Push auth processing
Use a single channel to handle both Push Auth and OTP results
Diffstat (limited to 'internal')
-rw-r--r--internal/command/twofactorverify/twofactorverify.go144
-rw-r--r--internal/command/twofactorverify/twofactorverify_test.go191
-rw-r--r--internal/command/twofactorverify/twofactorverifymanual_test.go154
-rw-r--r--internal/command/twofactorverify/twofactorverifypush_test.go144
-rw-r--r--internal/gitlabnet/twofactorverify/client.go24
-rw-r--r--internal/gitlabnet/twofactorverify/client_test.go208
-rw-r--r--internal/gitlabnet/twofactorverify/clientmanual_test.go151
-rw-r--r--internal/gitlabnet/twofactorverify/clientpush_test.go139
8 files changed, 448 insertions, 707 deletions
diff --git a/internal/command/twofactorverify/twofactorverify.go b/internal/command/twofactorverify/twofactorverify.go
index 1043bfb..4041de0 100644
--- a/internal/command/twofactorverify/twofactorverify.go
+++ b/internal/command/twofactorverify/twofactorverify.go
@@ -14,105 +14,63 @@ import (
"gitlab.com/gitlab-org/gitlab-shell/v14/internal/gitlabnet/twofactorverify"
)
+const (
+ timeout = 30 * time.Second
+ prompt = "OTP: "
+)
+
type Command struct {
Config *config.Config
- Client *twofactorverify.Client
Args *commandargs.Shell
ReadWriter *readwriter.ReadWriter
}
-type Result struct {
- Error error
- Status string
- Success bool
-}
-
func (c *Command) Execute(ctx context.Context) error {
- ctxlog := log.ContextLogger(ctx)
-
- // config.GetHTTPClient isn't thread-safe so save Client in struct for concurrency
- // workaround until #518 is fixed
- var err error
- c.Client, err = twofactorverify.NewClient(c.Config)
-
+ client, err := twofactorverify.NewClient(c.Config)
if err != nil {
- ctxlog.WithError(err).Error("twofactorverify: execute: OTP verification failed")
return err
}
- // Create timeout context
- // TODO: make timeout configurable
- const ctxTimeout = 30
- timeoutCtx, cancelTimeout := context.WithTimeout(ctx, ctxTimeout*time.Second)
- defer cancelTimeout()
+ ctx, cancel := context.WithTimeout(ctx, timeout)
+ defer cancel()
+
+ fmt.Fprint(c.ReadWriter.Out, prompt)
- // Background push notification with timeout
- pushauth := make(chan Result)
+ resultCh := make(chan string)
go func() {
- defer close(pushauth)
- ctxlog.Info("twofactorverify: execute: waiting for push auth")
- status, success, err := c.pushAuth(timeoutCtx)
- ctxlog.WithError(err).Info("twofactorverify: execute: push auth verified")
-
- select {
- case <-timeoutCtx.Done(): // push cancelled by manual OTP
- // skip writing to channel
- ctxlog.Info("twofactorverify: execute: push auth cancelled")
- default:
- pushauth <- Result{Error: err, Status: status, Success: success}
+ err := client.PushAuth(ctx, c.Args)
+ if err == nil {
+ resultCh <- "OTP has been validated by Push Authentication. Git operations are now allowed."
}
}()
- // Also allow manual OTP entry while waiting for push, with same timeout as push
- verify := make(chan Result)
go func() {
- defer close(verify)
- ctxlog.Info("twofactorverify: execute: waiting for user input")
- answer := ""
- answer = c.getOTP(timeoutCtx)
- ctxlog.Info("twofactorverify: execute: user input received")
-
- select {
- case <-timeoutCtx.Done(): // manual OTP cancelled by push
- // skip writing to channel
- ctxlog.Info("twofactorverify: execute: verify cancelled")
- default:
- ctxlog.Info("twofactorverify: execute: verifying entered OTP")
- status, success, err := c.verifyOTP(timeoutCtx, answer)
- ctxlog.WithError(err).Info("twofactorverify: execute: OTP verified")
- verify <- Result{Error: err, Status: status, Success: success}
+ answer, err := c.getOTP(ctx)
+ if err != nil {
+ resultCh <- formatErr(err)
}
- }()
- for {
- select {
- case res := <-verify:
- if res.Status == "" {
- // channel closed; don't print anything
- } else {
- fmt.Fprint(c.ReadWriter.Out, res.Status)
- return nil
- }
- case res := <-pushauth:
- if res.Status == "" {
- // channel closed; don't print anything
- } else {
- fmt.Fprint(c.ReadWriter.Out, res.Status)
- return nil
- }
- case <-timeoutCtx.Done(): // push timed out
- fmt.Fprint(c.ReadWriter.Out, "\nOTP verification timed out\n")
- return nil
+ if err := client.VerifyOTP(ctx, c.Args, answer); err != nil {
+ resultCh <- formatErr(err)
+ } else {
+ resultCh <- "OTP validation successful. Git operations are now allowed."
}
+ }()
+
+ var message string
+ select {
+ case message = <-resultCh:
+ case <-ctx.Done():
+ message = formatErr(ctx.Err())
}
+ log.WithContextFields(ctx, log.Fields{"message": message}).Info("Two factor verify command finished")
+ fmt.Fprintf(c.ReadWriter.Out, "\n%v\n", message)
+
return nil
}
-func (c *Command) getOTP(ctx context.Context) string {
- prompt := "OTP: "
- fmt.Fprint(c.ReadWriter.Out, prompt)
-
+func (c *Command) getOTP(ctx context.Context) (string, error) {
var answer string
otpLength := int64(64)
reader := io.LimitReader(c.ReadWriter.In, otpLength)
@@ -120,41 +78,13 @@ func (c *Command) getOTP(ctx context.Context) string {
log.ContextLogger(ctx).WithError(err).Debug("twofactorverify: getOTP: Failed to get user input")
}
- return answer
-}
-
-func (c *Command) verifyOTP(ctx context.Context, otp string) (status string, success bool, err error) {
- reason := ""
-
- success, reason, err = c.Client.VerifyOTP(ctx, c.Args, otp)
- if success {
- status = fmt.Sprintf("\nOTP validation successful. Git operations are now allowed.\n")
- } else {
- if err != nil {
- status = fmt.Sprintf("\nOTP validation failed.\n%v\n", err)
- } else {
- status = fmt.Sprintf("\nOTP validation failed.\n%v\n", reason)
- }
+ if answer == "" {
+ return "", fmt.Errorf("OTP cannot be blank.")
}
- err = nil
-
- return
+ return answer, nil
}
-func (c *Command) pushAuth(ctx context.Context) (status string, success bool, err error) {
- reason := ""
-
- success, reason, err = c.Client.PushAuth(ctx, c.Args)
- if success {
- status = fmt.Sprintf("\nPush OTP validation successful. Git operations are now allowed.\n")
- } else {
- if err != nil {
- status = fmt.Sprintf("\nPush OTP validation failed.\n%v\n", err)
- } else {
- status = fmt.Sprintf("\nPush OTP validation failed.\n%v\n", reason)
- }
- }
-
- return
+func formatErr(err error) string {
+ return fmt.Sprintf("OTP validation failed: %v", err)
}
diff --git a/internal/command/twofactorverify/twofactorverify_test.go b/internal/command/twofactorverify/twofactorverify_test.go
new file mode 100644
index 0000000..fcc095f
--- /dev/null
+++ b/internal/command/twofactorverify/twofactorverify_test.go
@@ -0,0 +1,191 @@
+package twofactorverify
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "io"
+ "net/http"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "gitlab.com/gitlab-org/gitlab-shell/v14/client/testserver"
+ "gitlab.com/gitlab-org/gitlab-shell/v14/internal/command/commandargs"
+ "gitlab.com/gitlab-org/gitlab-shell/v14/internal/command/readwriter"
+ "gitlab.com/gitlab-org/gitlab-shell/v14/internal/config"
+ "gitlab.com/gitlab-org/gitlab-shell/v14/internal/gitlabnet/twofactorverify"
+)
+
+type blockingReader struct{}
+
+func (*blockingReader) Read([]byte) (int, error) {
+ waitInfinitely := make(chan struct{})
+ <-waitInfinitely
+
+ return 0, nil
+}
+
+func setup(t *testing.T) []testserver.TestRequestHandler {
+ waitInfinitely := make(chan struct{})
+ requests := []testserver.TestRequestHandler{
+ {
+ Path: "/api/v4/internal/two_factor_manual_otp_check",
+ Handler: func(w http.ResponseWriter, r *http.Request) {
+ b, err := io.ReadAll(r.Body)
+ defer r.Body.Close()
+
+ require.NoError(t, err)
+
+ var requestBody *twofactorverify.RequestBody
+ require.NoError(t, json.Unmarshal(b, &requestBody))
+
+ switch requestBody.KeyId {
+ case "verify_via_otp", "verify_via_otp_with_push_error":
+ body := map[string]interface{}{
+ "success": true,
+ }
+ json.NewEncoder(w).Encode(body)
+ case "wait_infinitely":
+ <-waitInfinitely
+ case "error":
+ body := map[string]interface{}{
+ "success": false,
+ "message": "error message",
+ }
+ require.NoError(t, json.NewEncoder(w).Encode(body))
+ case "broken":
+ w.WriteHeader(http.StatusInternalServerError)
+ }
+ },
+ },
+ {
+ Path: "/api/v4/internal/two_factor_push_otp_check",
+ Handler: func(w http.ResponseWriter, r *http.Request) {
+ b, err := io.ReadAll(r.Body)
+ defer r.Body.Close()
+
+ require.NoError(t, err)
+
+ var requestBody *twofactorverify.RequestBody
+ require.NoError(t, json.Unmarshal(b, &requestBody))
+
+ switch requestBody.KeyId {
+ case "verify_via_push":
+ body := map[string]interface{}{
+ "success": true,
+ }
+ json.NewEncoder(w).Encode(body)
+ case "verify_via_otp_with_push_error":
+ w.WriteHeader(http.StatusInternalServerError)
+ default:
+ <-waitInfinitely
+ }
+ },
+ },
+ }
+
+ return requests
+}
+
+const errorHeader = "OTP validation failed: "
+
+func TestExecute(t *testing.T) {
+ requests := setup(t)
+
+ url := testserver.StartSocketHttpServer(t, requests)
+
+ testCases := []struct {
+ desc string
+ arguments *commandargs.Shell
+ input io.Reader
+ expectedOutput string
+ }{
+ {
+ desc: "Verify via OTP",
+ arguments: &commandargs.Shell{GitlabKeyId: "verify_via_otp"},
+ expectedOutput: "OTP validation successful. Git operations are now allowed.\n",
+ },
+ {
+ desc: "Verify via OTP",
+ arguments: &commandargs.Shell{GitlabKeyId: "verify_via_otp_with_push_error"},
+ expectedOutput: "OTP validation successful. Git operations are now allowed.\n",
+ },
+ {
+ desc: "Verify via push authentication",
+ arguments: &commandargs.Shell{GitlabKeyId: "verify_via_push"},
+ input: &blockingReader{},
+ expectedOutput: "OTP has been validated by Push Authentication. Git operations are now allowed.\n",
+ },
+ {
+ desc: "With an empty OTP",
+ arguments: &commandargs.Shell{GitlabKeyId: "verify_via_otp"},
+ input: bytes.NewBufferString("\n"),
+ expectedOutput: errorHeader + "OTP cannot be blank.\n",
+ },
+ {
+ desc: "With bad response",
+ arguments: &commandargs.Shell{GitlabKeyId: "-1"},
+ expectedOutput: errorHeader + "Parsing failed\n",
+ },
+ {
+ desc: "With API returns an error",
+ arguments: &commandargs.Shell{GitlabKeyId: "error"},
+ expectedOutput: errorHeader + "error message\n",
+ },
+ {
+ desc: "With API fails",
+ arguments: &commandargs.Shell{GitlabKeyId: "broken"},
+ expectedOutput: errorHeader + "Internal API error (500)\n",
+ },
+ {
+ desc: "With missing arguments",
+ arguments: &commandargs.Shell{},
+ expectedOutput: errorHeader + "who='' is invalid\n",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.desc, func(t *testing.T) {
+ output := &bytes.Buffer{}
+
+ input := tc.input
+ if input == nil {
+ input = bytes.NewBufferString("123456\n")
+ }
+
+ cmd := &Command{
+ Config: &config.Config{GitlabUrl: url},
+ Args: tc.arguments,
+ ReadWriter: &readwriter.ReadWriter{Out: output, In: input},
+ }
+
+ err := cmd.Execute(context.Background())
+
+ require.NoError(t, err)
+ require.Equal(t, prompt+"\n"+tc.expectedOutput, output.String())
+ })
+ }
+}
+
+func TestCanceledContext(t *testing.T) {
+ requests := setup(t)
+
+ output := &bytes.Buffer{}
+
+ url := testserver.StartSocketHttpServer(t, requests)
+ cmd := &Command{
+ Config: &config.Config{GitlabUrl: url},
+ Args: &commandargs.Shell{GitlabKeyId: "wait_infinitely"},
+ ReadWriter: &readwriter.ReadWriter{Out: output, In: &bytes.Buffer{}},
+ }
+
+ ctx, cancel := context.WithCancel(context.Background())
+
+ errCh := make(chan error)
+ go func() { errCh <- cmd.Execute(ctx) }()
+ cancel()
+
+ require.NoError(t, <-errCh)
+ require.Equal(t, prompt+"\n"+errorHeader+"context canceled\n", output.String())
+}
diff --git a/internal/command/twofactorverify/twofactorverifymanual_test.go b/internal/command/twofactorverify/twofactorverifymanual_test.go
deleted file mode 100644
index 59db281..0000000
--- a/internal/command/twofactorverify/twofactorverifymanual_test.go
+++ /dev/null
@@ -1,154 +0,0 @@
-package twofactorverify
-
-import (
- "bytes"
- "context"
- "encoding/json"
- "io"
- "net/http"
- "testing"
- "time"
-
- "github.com/stretchr/testify/require"
-
- "gitlab.com/gitlab-org/gitlab-shell/v14/client/testserver"
- "gitlab.com/gitlab-org/gitlab-shell/v14/internal/command/commandargs"
- "gitlab.com/gitlab-org/gitlab-shell/v14/internal/command/readwriter"
- "gitlab.com/gitlab-org/gitlab-shell/v14/internal/config"
- "gitlab.com/gitlab-org/gitlab-shell/v14/internal/gitlabnet/twofactorverify"
-)
-
-func setupManual(t *testing.T) []testserver.TestRequestHandler {
- requests := []testserver.TestRequestHandler{
- {
- Path: "/api/v4/internal/two_factor_manual_otp_check",
- Handler: func(w http.ResponseWriter, r *http.Request) {
- b, err := io.ReadAll(r.Body)
- defer r.Body.Close()
-
- require.NoError(t, err)
-
- var requestBody *twofactorverify.RequestBody
- require.NoError(t, json.Unmarshal(b, &requestBody))
-
- var body map[string]interface{}
- switch requestBody.KeyId {
- case "1":
- body = map[string]interface{}{
- "success": true,
- }
- json.NewEncoder(w).Encode(body)
- case "error":
- body = map[string]interface{}{
- "success": false,
- "message": "error message",
- }
- require.NoError(t, json.NewEncoder(w).Encode(body))
- case "broken":
- w.WriteHeader(http.StatusInternalServerError)
- }
- },
- },
- {
- Path: "/api/v4/internal/two_factor_push_otp_check",
- Handler: func(w http.ResponseWriter, r *http.Request) {
- b, err := io.ReadAll(r.Body)
- defer r.Body.Close()
-
- require.NoError(t, err)
-
- var requestBody *twofactorverify.RequestBody
- require.NoError(t, json.Unmarshal(b, &requestBody))
-
- time.Sleep(5 * time.Second)
-
- var body map[string]interface{}
- switch requestBody.KeyId {
- case "1":
- body = map[string]interface{}{
- "success": true,
- }
- json.NewEncoder(w).Encode(body)
- case "error":
- body = map[string]interface{}{
- "success": false,
- "message": "error message",
- }
- require.NoError(t, json.NewEncoder(w).Encode(body))
- case "broken":
- w.WriteHeader(http.StatusInternalServerError)
- default:
- body = map[string]interface{}{
- "success": true,
- "message": "default message",
- }
- json.NewEncoder(w).Encode(body)
- }
- },
- },
- }
-
- return requests
-}
-
-const (
- manualQuestion = "OTP: \n"
- manualErrorHeader = "OTP validation failed.\n"
-)
-
-func TestExecuteManual(t *testing.T) {
- requests := setupManual(t)
-
- url := testserver.StartSocketHttpServer(t, requests)
-
- testCases := []struct {
- desc string
- arguments *commandargs.Shell
- answer string
- expectedOutput string
- }{
- {
- desc: "With a known key id",
- arguments: &commandargs.Shell{GitlabKeyId: "1"},
- answer: "123456\n",
- expectedOutput: manualQuestion + "OTP validation successful. Git operations are now allowed.\n",
- },
- {
- desc: "With bad response",
- arguments: &commandargs.Shell{GitlabKeyId: "-1"},
- answer: "123456\n",
- expectedOutput: manualQuestion + manualErrorHeader + "Parsing failed\n",
- },
- {
- desc: "With API returns an error",
- arguments: &commandargs.Shell{GitlabKeyId: "error"},
- answer: "yes\n",
- expectedOutput: manualQuestion + manualErrorHeader + "error message\n",
- },
- {
- desc: "With API fails",
- arguments: &commandargs.Shell{GitlabKeyId: "broken"},
- answer: "yes\n",
- expectedOutput: manualQuestion + manualErrorHeader + "Internal API error (500)\n",
- },
- }
-
- for _, tc := range testCases {
- t.Run(tc.desc, func(t *testing.T) {
- output := &bytes.Buffer{}
-
- input := bytes.NewBufferString(tc.answer)
-
- cmd := &Command{
- Config: &config.Config{GitlabUrl: url},
- Args: tc.arguments,
- ReadWriter: &readwriter.ReadWriter{Out: output, In: input},
- }
-
- err := cmd.Execute(context.Background())
-
- require.NoError(t, err)
- require.Equal(t, tc.expectedOutput, output.String())
- })
- }
-}
diff --git a/internal/command/twofactorverify/twofactorverifypush_test.go b/internal/command/twofactorverify/twofactorverifypush_test.go
deleted file mode 100644
index 4b58cc5..0000000
--- a/internal/command/twofactorverify/twofactorverifypush_test.go
+++ /dev/null
@@ -1,144 +0,0 @@
-package twofactorverify
-
-import (
- "bytes"
- "context"
- "encoding/json"
- "io"
- "net/http"
- "testing"
- "time"
-
- "github.com/stretchr/testify/require"
-
- "gitlab.com/gitlab-org/gitlab-shell/v14/client/testserver"
- "gitlab.com/gitlab-org/gitlab-shell/v14/internal/command/commandargs"
- "gitlab.com/gitlab-org/gitlab-shell/v14/internal/command/readwriter"
- "gitlab.com/gitlab-org/gitlab-shell/v14/internal/config"
- "gitlab.com/gitlab-org/gitlab-shell/v14/internal/gitlabnet/twofactorverify"
-)
-
-func setupPush(t *testing.T) []testserver.TestRequestHandler {
- requests := []testserver.TestRequestHandler{
- {
- Path: "/api/v4/internal/two_factor_push_otp_check",
- Handler: func(w http.ResponseWriter, r *http.Request) {
- b, err := io.ReadAll(r.Body)
- defer r.Body.Close()
-
- require.NoError(t, err)
-
- var requestBody *twofactorverify.RequestBody
- require.NoError(t, json.Unmarshal(b, &requestBody))
-
- var body map[string]interface{}
- switch requestBody.KeyId {
- case "1":
- body = map[string]interface{}{
- "success": true,
- }
- json.NewEncoder(w).Encode(body)
- case "error":
- body = map[string]interface{}{
- "success": false,
- "message": "error message",
- }
- require.NoError(t, json.NewEncoder(w).Encode(body))
- case "broken":
- w.WriteHeader(http.StatusInternalServerError)
- }
- },
- },
- {
- Path: "/api/v4/internal/two_factor_manual_otp_check",
- Handler: func(w http.ResponseWriter, r *http.Request) {
- b, err := io.ReadAll(r.Body)
- defer r.Body.Close()
-
- require.NoError(t, err)
-
- var requestBody *twofactorverify.RequestBody
- require.NoError(t, json.Unmarshal(b, &requestBody))
-
- time.Sleep(20 * time.Second)
-
- var body map[string]interface{}
- switch requestBody.KeyId {
- case "1":
- body = map[string]interface{}{
- "success": true,
- }
- json.NewEncoder(w).Encode(body)
- case "error":
- body = map[string]interface{}{
- "success": false,
- "message": "error message",
- }
- require.NoError(t, json.NewEncoder(w).Encode(body))
- case "broken":
- w.WriteHeader(http.StatusInternalServerError)
- }
- },
- },
- }
-
- return requests
-}
-
-const (
- pushQuestion = "OTP: \n"
- pushErrorHeader = "Push OTP validation failed.\n"
-)
-
-func TestExecutePush(t *testing.T) {
- requests := setupPush(t)
-
- url := testserver.StartSocketHttpServer(t, requests)
-
- testCases := []struct {
- desc string
- arguments *commandargs.Shell
- answer string
- expectedOutput string
- }{
- {
- desc: "When push is provided",
- arguments: &commandargs.Shell{GitlabKeyId: "1"},
- answer: "",
- expectedOutput: pushQuestion + "Push OTP validation successful. Git operations are now allowed.\n",
- },
- {
- desc: "With API returns an error",
- arguments: &commandargs.Shell{GitlabKeyId: "error"},
- answer: "",
- expectedOutput: pushQuestion + pushErrorHeader + "error message\n",
- },
- {
- desc: "With API fails",
- arguments: &commandargs.Shell{GitlabKeyId: "broken"},
- answer: "",
- expectedOutput: pushQuestion + pushErrorHeader + "Internal API error (500)\n",
- },
- }
-
- for _, tc := range testCases {
- t.Run(tc.desc, func(t *testing.T) {
- output := &bytes.Buffer{}
-
- var input io.Reader
- // make input wait for push auth tests
- input, _ = io.Pipe()
-
- cmd := &Command{
- Config: &config.Config{GitlabUrl: url},
- Args: tc.arguments,
- ReadWriter: &readwriter.ReadWriter{Out: output, In: input},
- }
-
- err := cmd.Execute(context.Background())
-
- require.NoError(t, err)
- require.Equal(t, tc.expectedOutput, output.String())
- })
- }
-}
diff --git a/internal/gitlabnet/twofactorverify/client.go b/internal/gitlabnet/twofactorverify/client.go
index 9ab3ac6..24167c1 100644
--- a/internal/gitlabnet/twofactorverify/client.go
+++ b/internal/gitlabnet/twofactorverify/client.go
@@ -2,6 +2,7 @@ package twofactorverify
import (
"context"
+ "errors"
"fmt"
"net/http"
@@ -25,7 +26,7 @@ type Response struct {
type RequestBody struct {
KeyId string `json:"key_id,omitempty"`
UserId int64 `json:"user_id,omitempty"`
- OTPAttempt string `json:"otp_attempt"`
+ OTPAttempt string `json:"otp_attempt,omitempty"`
}
func NewClient(config *config.Config) (*Client, error) {
@@ -37,48 +38,47 @@ func NewClient(config *config.Config) (*Client, error) {
return &Client{config: config, client: client}, nil
}
-func (c *Client) VerifyOTP(ctx context.Context, args *commandargs.Shell, otp string) (bool, string, error) {
+func (c *Client) VerifyOTP(ctx context.Context, args *commandargs.Shell, otp string) error {
requestBody, err := c.getRequestBody(ctx, args, otp)
if err != nil {
- return false, "", err
+ return err
}
response, err := c.client.Post(ctx, "/two_factor_manual_otp_check", requestBody)
if err != nil {
- return false, "", err
+ return err
}
defer response.Body.Close()
return parse(response)
}
-func (c *Client) PushAuth(ctx context.Context, args *commandargs.Shell) (bool, string, error) {
- // enable push auth in internal rest api
+func (c *Client) PushAuth(ctx context.Context, args *commandargs.Shell) error {
requestBody, err := c.getRequestBody(ctx, args, "")
if err != nil {
- return false, "", err
+ return err
}
response, err := c.client.Post(ctx, "/two_factor_push_otp_check", requestBody)
if err != nil {
- return false, "", err
+ return err
}
defer response.Body.Close()
return parse(response)
}
-func parse(hr *http.Response) (bool, string, error) {
+func parse(hr *http.Response) error {
response := &Response{}
if err := gitlabnet.ParseJSON(hr, response); err != nil {
- return false, "", err
+ return err
}
if !response.Success {
- return false, response.Message, nil
+ return errors.New(response.Message)
}
- return true, response.Message, nil
+ return nil
}
func (c *Client) getRequestBody(ctx context.Context, args *commandargs.Shell, otp string) (*RequestBody, error) {
diff --git a/internal/gitlabnet/twofactorverify/client_test.go b/internal/gitlabnet/twofactorverify/client_test.go
new file mode 100644
index 0000000..9489b24
--- /dev/null
+++ b/internal/gitlabnet/twofactorverify/client_test.go
@@ -0,0 +1,208 @@
+package twofactorverify
+
+import (
+ "context"
+ "encoding/json"
+ "io"
+ "net/http"
+ "testing"
+
+ "gitlab.com/gitlab-org/gitlab-shell/v14/internal/gitlabnet/discover"
+
+ "github.com/stretchr/testify/require"
+ "gitlab.com/gitlab-org/gitlab-shell/v14/client"
+ "gitlab.com/gitlab-org/gitlab-shell/v14/client/testserver"
+ "gitlab.com/gitlab-org/gitlab-shell/v14/internal/command/commandargs"
+ "gitlab.com/gitlab-org/gitlab-shell/v14/internal/config"
+)
+
+func initialize(t *testing.T) []testserver.TestRequestHandler {
+ handler := func(w http.ResponseWriter, r *http.Request) {
+ b, err := io.ReadAll(r.Body)
+ defer r.Body.Close()
+
+ require.NoError(t, err)
+
+ var requestBody *RequestBody
+ require.NoError(t, json.Unmarshal(b, &requestBody))
+
+ switch requestBody.KeyId {
+ case "0":
+ body := map[string]interface{}{
+ "success": true,
+ }
+ require.NoError(t, json.NewEncoder(w).Encode(body))
+ case "1":
+ body := map[string]interface{}{
+ "success": false,
+ "message": "error message",
+ }
+ require.NoError(t, json.NewEncoder(w).Encode(body))
+ case "2":
+ w.WriteHeader(http.StatusForbidden)
+ body := &client.ErrorResponse{
+ Message: "Not allowed!",
+ }
+ require.NoError(t, json.NewEncoder(w).Encode(body))
+ case "3":
+ w.Write([]byte("{ \"message\": \"broken json!\""))
+ case "4":
+ w.WriteHeader(http.StatusForbidden)
+ }
+
+ if requestBody.UserId == 1 {
+ body := map[string]interface{}{
+ "success": true,
+ }
+ require.NoError(t, json.NewEncoder(w).Encode(body))
+ }
+ }
+
+ requests := []testserver.TestRequestHandler{
+ {
+ Path: "/api/v4/internal/two_factor_manual_otp_check",
+ Handler: handler,
+ },
+ {
+ Path: "/api/v4/internal/two_factor_push_otp_check",
+ Handler: handler,
+ },
+ {
+ Path: "/api/v4/internal/discover",
+ Handler: func(w http.ResponseWriter, r *http.Request) {
+ body := &discover.Response{
+ UserId: 1,
+ Username: "jane-doe",
+ Name: "Jane Doe",
+ }
+ require.NoError(t, json.NewEncoder(w).Encode(body))
+ },
+ },
+ }
+
+ return requests
+}
+
+const (
+ otpAttempt = "123456"
+)
+
+func TestVerifyOTPByKeyId(t *testing.T) {
+ client := setup(t)
+
+ args := &commandargs.Shell{GitlabKeyId: "0"}
+ err := client.VerifyOTP(context.Background(), args, otpAttempt)
+ require.NoError(t, err)
+}
+
+func TestVerifyOTPByUsername(t *testing.T) {
+ client := setup(t)
+
+ args := &commandargs.Shell{GitlabUsername: "jane-doe"}
+ err := client.VerifyOTP(context.Background(), args, otpAttempt)
+ require.NoError(t, err)
+}
+
+func TestErrorMessage(t *testing.T) {
+ client := setup(t)
+
+ args := &commandargs.Shell{GitlabKeyId: "1"}
+ err := client.VerifyOTP(context.Background(), args, otpAttempt)
+ require.Equal(t, "error message", err.Error())
+}
+
+func TestErrorResponses(t *testing.T) {
+ client := setup(t)
+
+ testCases := []struct {
+ desc string
+ fakeId string
+ expectedError string
+ }{
+ {
+ desc: "A response with an error message",
+ fakeId: "2",
+ expectedError: "Not allowed!",
+ },
+ {
+ desc: "A response with bad JSON",
+ fakeId: "3",
+ expectedError: "Parsing failed",
+ },
+ {
+ desc: "An error response without message",
+ fakeId: "4",
+ expectedError: "Internal API error (403)",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.desc, func(t *testing.T) {
+ args := &commandargs.Shell{GitlabKeyId: tc.fakeId}
+ err := client.VerifyOTP(context.Background(), args, otpAttempt)
+
+ require.EqualError(t, err, tc.expectedError)
+ })
+ }
+}
+
+func TestVerifyPush(t *testing.T) {
+ client := setup(t)
+
+ args := &commandargs.Shell{GitlabKeyId: "0"}
+ err := client.PushAuth(context.Background(), args)
+ require.NoError(t, err)
+}
+
+func TestErrorMessagePush(t *testing.T) {
+ client := setup(t)
+
+ args := &commandargs.Shell{GitlabKeyId: "1"}
+ err := client.PushAuth(context.Background(), args)
+ require.Equal(t, "error message", err.Error())
+}
+
+func TestErrorResponsesPush(t *testing.T) {
+ client := setup(t)
+
+ testCases := []struct {
+ desc string
+ fakeId string
+ expectedError string
+ }{
+ {
+ desc: "A response with an error message",
+ fakeId: "2",
+ expectedError: "Not allowed!",
+ },
+ {
+ desc: "A response with bad JSON",
+ fakeId: "3",
+ expectedError: "Parsing failed",
+ },
+ {
+ desc: "An error response without message",
+ fakeId: "4",
+ expectedError: "Internal API error (403)",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.desc, func(t *testing.T) {
+ args := &commandargs.Shell{GitlabKeyId: tc.fakeId}
+ err := client.PushAuth(context.Background(), args)
+
+ require.EqualError(t, err, tc.expectedError)
+ })
+ }
+}
+
+func setup(t *testing.T) *Client {
+ requests := initialize(t)
+ url := testserver.StartSocketHttpServer(t, requests)
+
+ client, err := NewClient(&config.Config{GitlabUrl: url})
+ require.NoError(t, err)
+
+ return client
+}
diff --git a/internal/gitlabnet/twofactorverify/clientmanual_test.go b/internal/gitlabnet/twofactorverify/clientmanual_test.go
deleted file mode 100644
index 6324165..0000000
--- a/internal/gitlabnet/twofactorverify/clientmanual_test.go
+++ /dev/null
@@ -1,151 +0,0 @@
-package twofactorverify
-
-import (
- "context"
- "encoding/json"
- "io"
- "net/http"
- "testing"
-
- "gitlab.com/gitlab-org/gitlab-shell/v14/internal/gitlabnet/discover"
-
- "github.com/stretchr/testify/require"
- "gitlab.com/gitlab-org/gitlab-shell/v14/client"
- "gitlab.com/gitlab-org/gitlab-shell/v14/client/testserver"
- "gitlab.com/gitlab-org/gitlab-shell/v14/internal/command/commandargs"
- "gitlab.com/gitlab-org/gitlab-shell/v14/internal/config"
-)
-
-func initializeManual(t *testing.T) []testserver.TestRequestHandler {
- requests := []testserver.TestRequestHandler{
- {
- Path: "/api/v4/internal/two_factor_manual_otp_check",
- Handler: func(w http.ResponseWriter, r *http.Request) {
- b, err := io.ReadAll(r.Body)
- defer r.Body.Close()
-
- require.NoError(t, err)
-
- var requestBody *RequestBody
- require.NoError(t, json.Unmarshal(b, &requestBody))
-
- switch requestBody.KeyId {
- case "0":
- body := map[string]interface{}{
- "success": true,
- }
- require.NoError(t, json.NewEncoder(w).Encode(body))
- case "1":
- body := map[string]interface{}{
- "success": false,
- "message": "error message",
- }
- require.NoError(t, json.NewEncoder(w).Encode(body))
- case "2":
- w.WriteHeader(http.StatusForbidden)
- body := &client.ErrorResponse{
- Message: "Not allowed!",
- }
- require.NoError(t, json.NewEncoder(w).Encode(body))
- case "3":
- w.Write([]byte("{ \"message\": \"broken json!\""))
- case "4":
- w.WriteHeader(http.StatusForbidden)
- }
-
- if requestBody.UserId == 1 {
- body := map[string]interface{}{
- "success": true,
- }
- require.NoError(t, json.NewEncoder(w).Encode(body))
- }
- },
- },
- {
- Path: "/api/v4/internal/discover",
- Handler: func(w http.ResponseWriter, r *http.Request) {
- body := &discover.Response{
- UserId: 1,
- Username: "jane-doe",
- Name: "Jane Doe",
- }
- require.NoError(t, json.NewEncoder(w).Encode(body))
- },
- },
- }
-
- return requests
-}
-
-const (
- manualOtpAttempt = "123456"
-)
-
-func TestVerifyOTPByKeyId(t *testing.T) {
- client := setupManual(t)
-
- args := &commandargs.Shell{GitlabKeyId: "0"}
- _, _, err := client.VerifyOTP(context.Background(), args, manualOtpAttempt)
- require.NoError(t, err)
-}
-
-func TestVerifyOTPByUsername(t *testing.T) {
- client := setupManual(t)
-
- args := &commandargs.Shell{GitlabUsername: "jane-doe"}
- _, _, err := client.VerifyOTP(context.Background(), args, manualOtpAttempt)
- require.NoError(t, err)
-}
-
-func TestErrorMessage(t *testing.T) {
- client := setupManual(t)
-
- args := &commandargs.Shell{GitlabKeyId: "1"}
- _, reason, _ := client.VerifyOTP(context.Background(), args, manualOtpAttempt)
- require.Equal(t, "error message", reason)
-}
-
-func TestErrorResponses(t *testing.T) {
- client := setupManual(t)
-
- testCases := []struct {
- desc string
- fakeId string
- expectedError string
- }{
- {
- desc: "A response with an error message",
- fakeId: "2",
- expectedError: "Not allowed!",
- },
- {
- desc: "A response with bad JSON",
- fakeId: "3",
- expectedError: "Parsing failed",
- },
- {
- desc: "An error response without message",
- fakeId: "4",
- expectedError: "Internal API error (403)",
- },
- }
-
- for _, tc := range testCases {
- t.Run(tc.desc, func(t *testing.T) {
- args := &commandargs.Shell{GitlabKeyId: tc.fakeId}
- _, _, err := client.VerifyOTP(context.Background(), args, manualOtpAttempt)
-
- require.EqualError(t, err, tc.expectedError)
- })
- }
-}
-
-func setupManual(t *testing.T) *Client {
- requests := initializeManual(t)
- url := testserver.StartSocketHttpServer(t, requests)
-
- client, err := NewClient(&config.Config{GitlabUrl: url})
- require.NoError(t, err)
-
- return client
-}
diff --git a/internal/gitlabnet/twofactorverify/clientpush_test.go b/internal/gitlabnet/twofactorverify/clientpush_test.go
deleted file mode 100644
index d1624e6..0000000
--- a/internal/gitlabnet/twofactorverify/clientpush_test.go
+++ /dev/null
@@ -1,139 +0,0 @@
-package twofactorverify
-
-import (
- "context"
- "encoding/json"
- "io"
- "net/http"
- "testing"
-
- "gitlab.com/gitlab-org/gitlab-shell/v14/internal/gitlabnet/discover"
-
- "github.com/stretchr/testify/require"
- "gitlab.com/gitlab-org/gitlab-shell/v14/client"
- "gitlab.com/gitlab-org/gitlab-shell/v14/client/testserver"
- "gitlab.com/gitlab-org/gitlab-shell/v14/internal/command/commandargs"
- "gitlab.com/gitlab-org/gitlab-shell/v14/internal/config"
-)
-
-func initializePush(t *testing.T) []testserver.TestRequestHandler {
- requests := []testserver.TestRequestHandler{
- {
- Path: "/api/v4/internal/two_factor_push_otp_check",
- Handler: func(w http.ResponseWriter, r *http.Request) {
- b, err := io.ReadAll(r.Body)
- defer r.Body.Close()
-
- require.NoError(t, err)
-
- var requestBody *RequestBody
- require.NoError(t, json.Unmarshal(b, &requestBody))
-
- switch requestBody.KeyId {
- case "0":
- body := map[string]interface{}{
- "success": true,
- }
- require.NoError(t, json.NewEncoder(w).Encode(body))
- case "1":
- body := map[string]interface{}{
- "success": false,
- "message": "error message",
- }
- require.NoError(t, json.NewEncoder(w).Encode(body))
- case "2":
- w.WriteHeader(http.StatusForbidden)
- body := &client.ErrorResponse{
- Message: "Not allowed!",
- }
- require.NoError(t, json.NewEncoder(w).Encode(body))
- case "3":
- w.Write([]byte("{ \"message\": \"broken json!\""))
- case "4":
- w.WriteHeader(http.StatusForbidden)
- }
-
- if requestBody.UserId == 1 {
- body := map[string]interface{}{
- "success": true,
- }
- require.NoError(t, json.NewEncoder(w).Encode(body))
- }
- },
- },
- {
- Path: "/api/v4/internal/discover",
- Handler: func(w http.ResponseWriter, r *http.Request) {
- body := &discover.Response{
- UserId: 1,
- Username: "jane-doe",
- Name: "Jane Doe",
- }
- require.NoError(t, json.NewEncoder(w).Encode(body))
- },
- },
- }
-
- return requests
-}
-
-func TestVerifyPush(t *testing.T) {
- client := setupPush(t)
-
- args := &commandargs.Shell{GitlabKeyId: "0"}
- _, _, err := client.PushAuth(context.Background(), args)
- require.NoError(t, err)
-}
-
-func TestErrorMessagePush(t *testing.T) {
- client := setupPush(t)
-
- args := &commandargs.Shell{GitlabKeyId: "1"}
- _, reason, _ := client.PushAuth(context.Background(), args)
- require.Equal(t, "error message", reason)
-}
-
-func TestErrorResponsesPush(t *testing.T) {
- client := setupPush(t)
-
- testCases := []struct {
- desc string
- fakeId string
- expectedError string
- }{
- {
- desc: "A response with an error message",
- fakeId: "2",
- expectedError: "Not allowed!",
- },
- {
- desc: "A response with bad JSON",
- fakeId: "3",
- expectedError: "Parsing failed",
- },
- {
- desc: "An error response without message",
- fakeId: "4",
- expectedError: "Internal API error (403)",
- },
- }
-
- for _, tc := range testCases {
- t.Run(tc.desc, func(t *testing.T) {
- args := &commandargs.Shell{GitlabKeyId: tc.fakeId}
- _, _, err := client.PushAuth(context.Background(), args)
-
- require.EqualError(t, err, tc.expectedError)
- })
- }
-}
-
-func setupPush(t *testing.T) *Client {
- requests := initializePush(t)
- url := testserver.StartSocketHttpServer(t, requests)
-
- client, err := NewClient(&config.Config{GitlabUrl: url})
- require.NoError(t, err)
-
- return client
-}