From 8a16b50a2c315d17b6a6ca1bc654f51563011c29 Mon Sep 17 00:00:00 2001 From: kmcknight <kmcknight@gitlab.com> Date: Thu, 25 Feb 2021 17:17:25 -0800 Subject: [PATCH 01/25] Add prototype code written by Fortinet --- .../twofactorverify/twofactorverify.go | 82 ++++++++++++++++--- internal/gitlabnet/twofactorverify/client.go | 40 ++++++--- 2 files changed, 99 insertions(+), 23 deletions(-) diff --git a/internal/command/twofactorverify/twofactorverify.go b/internal/command/twofactorverify/twofactorverify.go index b1c55088..02b5ac8c 100644 --- a/internal/command/twofactorverify/twofactorverify.go +++ b/internal/command/twofactorverify/twofactorverify.go @@ -17,12 +17,44 @@ type Command struct { ReadWriter *readwriter.ReadWriter } +type Result struct { + Error error + Status string + Success bool +} + func (c *Command) Execute(ctx context.Context) error { - err := c.verifyOTP(ctx, c.getOTP()) - if err != nil { - return err - } + verify := make(chan Result) + pushauth := make(chan Result) + + go func() { + status, success, err := c.verifyOTP(ctx, c.getOTP()) + verify <- Result{Error: err, Status: status, Success: success} + }() + go func() { + status, success, err := c.pushAuth(ctx) + pushauth <- Result{Error: err, Status: status, Success: success} + }() + +L: + for { + select { + case res := <-verify: + if res.Error != nil { + return res.Error + } + fmt.Fprint(c.ReadWriter.Out, res.Status) + break L + case res := <-pushauth: + if res.Success { + fmt.Fprint(c.ReadWriter.Out, res.Status) + break L + } else { + // ignore reject from remote, need to wait for user input in this case + } + } + } return nil } @@ -38,18 +70,46 @@ func (c *Command) getOTP() string { return answer } -func (c *Command) verifyOTP(ctx context.Context, otp string) error { +func (c *Command) pushAuth(ctx context.Context) (status string, success bool, err error) { client, err := twofactorverify.NewClient(c.Config) if err != nil { - return err + return "", false, err } - err = client.VerifyOTP(ctx, c.Args, otp) - if err == nil { - fmt.Fprint(c.ReadWriter.Out, "\nOTP validation successful. Git operations are now allowed.\n") + reason := "" + + success, reason, err = client.PushAuth(ctx, c.Args) + if success { + status = fmt.Sprintf("\nPush OTP validation successful. Git operations are now allowed.\n") } else { - fmt.Fprintf(c.ReadWriter.Out, "\nOTP validation failed.\n%v\n", err) + 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 nil + return +} + +func (c *Command) verifyOTP(ctx context.Context, otp string) (status string, success bool, err error) { + client, err := twofactorverify.NewClient(c.Config) + if err != nil { + return "", false, err + } + + reason := "" + + success, reason, err = 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) + } + } + + return } diff --git a/internal/gitlabnet/twofactorverify/client.go b/internal/gitlabnet/twofactorverify/client.go index aab302be..cf22cf8f 100644 --- a/internal/gitlabnet/twofactorverify/client.go +++ b/internal/gitlabnet/twofactorverify/client.go @@ -2,7 +2,6 @@ package twofactorverify import ( "context" - "errors" "fmt" "net/http" @@ -27,6 +26,7 @@ type RequestBody struct { KeyId string `json:"key_id,omitempty"` UserId int64 `json:"user_id,omitempty"` OTPAttempt string `json:"otp_attempt"` + PushAuth bool `json:"push_auth"` } func NewClient(config *config.Config) (*Client, error) { @@ -38,35 +38,51 @@ 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) error { - requestBody, err := c.getRequestBody(ctx, args, otp) +func (c *Client) VerifyOTP(ctx context.Context, args *commandargs.Shell, otp string) (bool, string, error) { + requestBody, err := c.getRequestBody(ctx, args, otp, false) if err != nil { - return err + return false, "", err } response, err := c.client.Post(ctx, "/two_factor_otp_check", requestBody) if err != nil { - return err + return false, "", err } defer response.Body.Close() return parse(response) } -func parse(hr *http.Response) error { +func (c *Client) PushAuth(ctx context.Context, args *commandargs.Shell) (bool, string, error) { + // enable push auth in internal rest api + requestBody, err := c.getRequestBody(ctx, args, "", true) + if err != nil { + return false, "", err + } + + response, err := c.client.Post(ctx, "/two_factor_otp_check", requestBody) + if err != nil { + return false, "", err + } + defer response.Body.Close() + + return parse(response) +} + +func parse(hr *http.Response) (bool, string, error) { response := &Response{} if err := gitlabnet.ParseJSON(hr, response); err != nil { - return err + return false, "", err } if !response.Success { - return errors.New(response.Message) + return false, response.Message, nil } - return nil + return true, response.Message, nil } -func (c *Client) getRequestBody(ctx context.Context, args *commandargs.Shell, otp string) (*RequestBody, error) { +func (c *Client) getRequestBody(ctx context.Context, args *commandargs.Shell, otp string, pushauth bool) (*RequestBody, error) { client, err := discover.NewClient(c.config) if err != nil { @@ -75,7 +91,7 @@ func (c *Client) getRequestBody(ctx context.Context, args *commandargs.Shell, ot var requestBody *RequestBody if args.GitlabKeyId != "" { - requestBody = &RequestBody{KeyId: args.GitlabKeyId, OTPAttempt: otp} + requestBody = &RequestBody{KeyId: args.GitlabKeyId, OTPAttempt: otp, PushAuth: pushauth} } else { userInfo, err := client.GetByCommandArgs(ctx, args) @@ -83,7 +99,7 @@ func (c *Client) getRequestBody(ctx context.Context, args *commandargs.Shell, ot return nil, err } - requestBody = &RequestBody{UserId: userInfo.UserId, OTPAttempt: otp} + requestBody = &RequestBody{UserId: userInfo.UserId, OTPAttempt: otp, PushAuth: pushauth} } return requestBody, nil -- GitLab From 4636c165b2c76d0dce626f54d4257453e2f70e3e Mon Sep 17 00:00:00 2001 From: kmcknight <kmcknight@gitlab.com> Date: Fri, 26 Mar 2021 16:28:56 -0700 Subject: [PATCH 02/25] Fix incorrect return code --- internal/command/twofactorverify/twofactorverify.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/command/twofactorverify/twofactorverify.go b/internal/command/twofactorverify/twofactorverify.go index 02b5ac8c..c6fd14a3 100644 --- a/internal/command/twofactorverify/twofactorverify.go +++ b/internal/command/twofactorverify/twofactorverify.go @@ -111,5 +111,7 @@ func (c *Command) verifyOTP(ctx context.Context, otp string) (status string, suc } } + err = nil + return } -- GitLab From 6b6b6e0d9f491bded5b6e8f3caad5182fb3b2f9f Mon Sep 17 00:00:00 2001 From: kmcknight <kmcknight@gitlab.com> Date: Fri, 26 Mar 2021 16:29:12 -0700 Subject: [PATCH 03/25] Update tests to handle push notification implementation --- .../twofactorverify/twofactorverify_test.go | 33 ++++++++++++++++--- .../gitlabnet/twofactorverify/client_test.go | 10 +++--- spec/gitlab_shell_two_factor_verify_spec.rb | 28 +++++++++++++++- 3 files changed, 61 insertions(+), 10 deletions(-) diff --git a/internal/command/twofactorverify/twofactorverify_test.go b/internal/command/twofactorverify/twofactorverify_test.go index 9d5f54d8..2ee02265 100644 --- a/internal/command/twofactorverify/twofactorverify_test.go +++ b/internal/command/twofactorverify/twofactorverify_test.go @@ -7,6 +7,7 @@ import ( "io/ioutil" "net/http" "testing" + "time" "github.com/stretchr/testify/require" @@ -30,14 +31,31 @@ func setup(t *testing.T) []testserver.TestRequestHandler { 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, + if requestBody.PushAuth { + body = map[string]interface{}{ + "success": false, + } + } else { + body = map[string]interface{}{ + "success": true, + } + } + json.NewEncoder(w).Encode(body) + case "2": + if requestBody.PushAuth { + body = map[string]interface{}{ + "success": true, + } + } else { + // Stall verifyOTP long enough for pushAuth to complete + time.Sleep(10 * time.Second) } json.NewEncoder(w).Encode(body) case "error": - body := map[string]interface{}{ + body = map[string]interface{}{ "success": false, "message": "error message", } @@ -69,6 +87,13 @@ func TestExecute(t *testing.T) { answer string expectedOutput string }{ + { + desc: "When push is provided", + arguments: &commandargs.Shell{GitlabKeyId: "2"}, + answer: "", + expectedOutput: question + + "Push OTP validation successful. Git operations are now allowed.\n", + }, { desc: "With a known key id", arguments: &commandargs.Shell{GitlabKeyId: "1"}, @@ -105,8 +130,8 @@ func TestExecute(t *testing.T) { for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { output := &bytes.Buffer{} - input := bytes.NewBufferString(tc.answer) + input := bytes.NewBufferString(tc.answer) cmd := &Command{ Config: &config.Config{GitlabUrl: url}, Args: tc.arguments, diff --git a/internal/gitlabnet/twofactorverify/client_test.go b/internal/gitlabnet/twofactorverify/client_test.go index 7bb037ed..22f59891 100644 --- a/internal/gitlabnet/twofactorverify/client_test.go +++ b/internal/gitlabnet/twofactorverify/client_test.go @@ -85,7 +85,7 @@ func TestVerifyOTPByKeyId(t *testing.T) { defer cleanup() args := &commandargs.Shell{GitlabKeyId: "0"} - err := client.VerifyOTP(context.Background(), args, otpAttempt) + _, _, err := client.VerifyOTP(context.Background(), args, otpAttempt) require.NoError(t, err) } @@ -94,7 +94,7 @@ func TestVerifyOTPByUsername(t *testing.T) { defer cleanup() args := &commandargs.Shell{GitlabUsername: "jane-doe"} - err := client.VerifyOTP(context.Background(), args, otpAttempt) + _, _, err := client.VerifyOTP(context.Background(), args, otpAttempt) require.NoError(t, err) } @@ -103,8 +103,8 @@ func TestErrorMessage(t *testing.T) { defer cleanup() args := &commandargs.Shell{GitlabKeyId: "1"} - err := client.VerifyOTP(context.Background(), args, otpAttempt) - require.Equal(t, "error message", err.Error()) + _, reason, _ := client.VerifyOTP(context.Background(), args, otpAttempt) + require.Equal(t, "error message", reason) } func TestErrorResponses(t *testing.T) { @@ -136,7 +136,7 @@ func TestErrorResponses(t *testing.T) { 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) + _, _, err := client.VerifyOTP(context.Background(), args, otpAttempt) require.EqualError(t, err, tc.expectedError) }) diff --git a/spec/gitlab_shell_two_factor_verify_spec.rb b/spec/gitlab_shell_two_factor_verify_spec.rb index 25d88698..244d7d85 100644 --- a/spec/gitlab_shell_two_factor_verify_spec.rb +++ b/spec/gitlab_shell_two_factor_verify_spec.rb @@ -24,7 +24,15 @@ describe 'bin/gitlab-shell 2fa_verify' do key_id = params['key_id'] || params['user_id'].to_s if key_id == '100' - res.body = { success: true }.to_json + if params['push_auth'] + res.body = { success: false }.to_json + else + res.body = { success: true }.to_json + end + elsif key_id == '102' + if params['push_auth'] + res.body = { success: true }.to_json + end else res.body = { success: false, message: 'boom!' }.to_json end @@ -38,6 +46,14 @@ describe 'bin/gitlab-shell 2fa_verify' do end describe 'command' do + context 'when push is provided' do + let(:cmd) { "#{gitlab_shell_path} key-102" } + + it 'prints a successful push verification message' do + verify_successful_verification_push!(cmd) + end + end + context 'when key is provided' do let(:cmd) { "#{gitlab_shell_path} key-100" } @@ -78,4 +94,14 @@ describe 'bin/gitlab-shell 2fa_verify' do expect(stdout.flush.read).to eq("\nOTP validation successful. Git operations are now allowed.\n") end end + + def verify_successful_verification_push!(cmd) + Open3.popen2(env, cmd) do |stdin, stdout| + expect(stdout.gets(5)).to eq('OTP: ') + + stdin.puts('123456') + + expect(stdout.flush.read).to eq("\nPush OTP validation successful. Git operations are now allowed.\n") + end + end end -- GitLab From 3ec31c34ae9c3fbf1dc70361ba25474bf3a30a62 Mon Sep 17 00:00:00 2001 From: kmcknight <kmcknight@gitlab.com> Date: Thu, 8 Apr 2021 18:57:27 -0700 Subject: [PATCH 04/25] Fix test race condition --- .../twofactorverify/twofactorverify.go | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/internal/command/twofactorverify/twofactorverify.go b/internal/command/twofactorverify/twofactorverify.go index c6fd14a3..9f118955 100644 --- a/internal/command/twofactorverify/twofactorverify.go +++ b/internal/command/twofactorverify/twofactorverify.go @@ -13,6 +13,7 @@ import ( type Command struct { Config *config.Config + Client *twofactorverify.Client Args *commandargs.Shell ReadWriter *readwriter.ReadWriter } @@ -24,6 +25,14 @@ type Result struct { } func (c *Command) Execute(ctx context.Context) error { + // 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) + if err != nil { + return err + } + verify := make(chan Result) pushauth := make(chan Result) @@ -71,14 +80,9 @@ func (c *Command) getOTP() string { } func (c *Command) pushAuth(ctx context.Context) (status string, success bool, err error) { - client, err := twofactorverify.NewClient(c.Config) - if err != nil { - return "", false, err - } - reason := "" - success, reason, err = client.PushAuth(ctx, c.Args) + 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 { @@ -93,14 +97,9 @@ func (c *Command) pushAuth(ctx context.Context) (status string, success bool, er } func (c *Command) verifyOTP(ctx context.Context, otp string) (status string, success bool, err error) { - client, err := twofactorverify.NewClient(c.Config) - if err != nil { - return "", false, err - } - reason := "" - success, reason, err = client.VerifyOTP(ctx, c.Args, otp) + 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 { -- GitLab From 582a8bd0923a6b4fd3521786ed16e07390973921 Mon Sep 17 00:00:00 2001 From: kmcknight <kmcknight@gitlab.com> Date: Tue, 4 Jan 2022 12:20:56 -0800 Subject: [PATCH 05/25] Handle unbuffered channels per review comment --- .../twofactorverify/twofactorverify.go | 73 ++++++++++--------- 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/internal/command/twofactorverify/twofactorverify.go b/internal/command/twofactorverify/twofactorverify.go index 9f118955..78777450 100644 --- a/internal/command/twofactorverify/twofactorverify.go +++ b/internal/command/twofactorverify/twofactorverify.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "io" + "time" "gitlab.com/gitlab-org/gitlab-shell/internal/command/commandargs" "gitlab.com/gitlab-org/gitlab-shell/internal/command/readwriter" @@ -33,41 +34,41 @@ func (c *Command) Execute(ctx context.Context) error { return err } - verify := make(chan Result) - pushauth := make(chan Result) + // Create timeout context + // TODO: make timeout configurable + const ctxTimeout = 30 + timeoutCtx, cancel := context.WithTimeout(ctx, ctxTimeout * time.Second) + defer cancel() + // Background push notification with timeout + pushauth := make(chan Result) go func() { - status, success, err := c.verifyOTP(ctx, c.getOTP()) - verify <- Result{Error: err, Status: status, Success: success} + defer close(pushauth) + status, success, err := c.pushAuth(timeoutCtx) + pushauth <- Result{Error: err, Status: status, Success: success} }() + // Also allow manual OTP entry while waiting for push, with same timeout as push + verify := make(chan Result) go func() { - status, success, err := c.pushAuth(ctx) - pushauth <- Result{Error: err, Status: status, Success: success} + defer close(verify) + status, success, err := c.verifyOTP(timeoutCtx, c.getOTP(timeoutCtx)) + verify <- Result{Error: err, Status: status, Success: success} }() -L: - for { - select { - case res := <-verify: - if res.Error != nil { - return res.Error - } - fmt.Fprint(c.ReadWriter.Out, res.Status) - break L - case res := <-pushauth: - if res.Success { - fmt.Fprint(c.ReadWriter.Out, res.Status) - break L - } else { - // ignore reject from remote, need to wait for user input in this case - } - } + select { + case res := <-verify: // manual OTP + fmt.Fprint(c.ReadWriter.Out, res.Status) + case res := <-pushauth: // push + fmt.Fprint(c.ReadWriter.Out, res.Status) + case <-timeoutCtx.Done(): // push timed out + fmt.Fprint(c.ReadWriter.Out, "OTP verification timed out") } + return nil } -func (c *Command) getOTP() string { +func (c *Command) getOTP(ctx context.Context) string { prompt := "OTP: " fmt.Fprint(c.ReadWriter.Out, prompt) @@ -79,38 +80,38 @@ func (c *Command) getOTP() string { return answer } -func (c *Command) pushAuth(ctx context.Context) (status string, success bool, err error) { +func (c *Command) verifyOTP(ctx context.Context, otp string) (status string, success bool, err error) { reason := "" - success, reason, err = c.Client.PushAuth(ctx, c.Args) + success, reason, err = c.Client.VerifyOTP(ctx, c.Args, otp) if success { - status = fmt.Sprintf("\nPush OTP validation successful. Git operations are now allowed.\n") + status = fmt.Sprintf("\nOTP validation successful. Git operations are now allowed.\n") } else { if err != nil { - status = fmt.Sprintf("\nPush OTP validation failed.\n%v\n", err) + status = fmt.Sprintf("\nOTP validation failed.\n%v\n", err) } else { - status = fmt.Sprintf("\nPush OTP validation failed.\n%v\n", reason) + status = fmt.Sprintf("\nOTP validation failed.\n%v\n", reason) } } + err = nil + return } -func (c *Command) verifyOTP(ctx context.Context, otp string) (status string, success bool, err error) { +func (c *Command) pushAuth(ctx context.Context) (status string, success bool, err error) { reason := "" - success, reason, err = c.Client.VerifyOTP(ctx, c.Args, otp) + success, reason, err = c.Client.PushAuth(ctx, c.Args) if success { - status = fmt.Sprintf("\nOTP validation successful. Git operations are now allowed.\n") + status = fmt.Sprintf("\nPush OTP validation successful. Git operations are now allowed.\n") } else { if err != nil { - status = fmt.Sprintf("\nOTP validation failed.\n%v\n", err) + status = fmt.Sprintf("\nPush OTP validation failed.\n%v\n", err) } else { - status = fmt.Sprintf("\nOTP validation failed.\n%v\n", reason) + status = fmt.Sprintf("\nPush OTP validation failed.\n%v\n", reason) } } - err = nil - return } -- GitLab From 19822b942d6a7aebb34058ea16cbf537934d8989 Mon Sep 17 00:00:00 2001 From: kmcknight <kmcknight@gitlab.com> Date: Thu, 13 Jan 2022 12:43:53 -0800 Subject: [PATCH 06/25] Fix case formatting --- internal/command/twofactorverify/twofactorverify.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/command/twofactorverify/twofactorverify.go b/internal/command/twofactorverify/twofactorverify.go index 78777450..389022d0 100644 --- a/internal/command/twofactorverify/twofactorverify.go +++ b/internal/command/twofactorverify/twofactorverify.go @@ -59,7 +59,7 @@ func (c *Command) Execute(ctx context.Context) error { select { case res := <-verify: // manual OTP fmt.Fprint(c.ReadWriter.Out, res.Status) - case res := <-pushauth: // push + case res := <-pushauth: // push fmt.Fprint(c.ReadWriter.Out, res.Status) case <-timeoutCtx.Done(): // push timed out fmt.Fprint(c.ReadWriter.Out, "OTP verification timed out") -- GitLab From b38bbf3f57848588c03c20e97fda846831113282 Mon Sep 17 00:00:00 2001 From: kmcknight <kmcknight@gitlab.com> Date: Fri, 21 Jan 2022 17:31:19 -0800 Subject: [PATCH 07/25] Checkpoint: I do believe it's working --- .../twofactorverify/twofactorverify.go | 61 +++++++++++++++---- 1 file changed, 48 insertions(+), 13 deletions(-) diff --git a/internal/command/twofactorverify/twofactorverify.go b/internal/command/twofactorverify/twofactorverify.go index 389022d0..e2af137f 100644 --- a/internal/command/twofactorverify/twofactorverify.go +++ b/internal/command/twofactorverify/twofactorverify.go @@ -37,32 +37,67 @@ func (c *Command) Execute(ctx context.Context) error { // Create timeout context // TODO: make timeout configurable const ctxTimeout = 30 - timeoutCtx, cancel := context.WithTimeout(ctx, ctxTimeout * time.Second) - defer cancel() + timeoutCtx, cancelTimeout := context.WithTimeout(ctx, ctxTimeout * time.Second) + verifyCtx, cancelVerify := context.WithCancel(timeoutCtx) + pushCtx, cancelPush := context.WithCancel(timeoutCtx) + defer cancelTimeout() // Background push notification with timeout pushauth := make(chan Result) go func() { defer close(pushauth) - status, success, err := c.pushAuth(timeoutCtx) - pushauth <- Result{Error: err, Status: status, Success: success} + status, success, err := c.pushAuth(pushCtx) + + select { + case <-pushCtx.Done(): // push cancelled by manual OTP + pushauth <- Result{Error: nil, Status: "cancelled", Success: false} + default: + pushauth <- Result{Error: err, Status: status, Success: success} + cancelVerify() + } }() // Also allow manual OTP entry while waiting for push, with same timeout as push verify := make(chan Result) go func() { defer close(verify) - status, success, err := c.verifyOTP(timeoutCtx, c.getOTP(timeoutCtx)) - verify <- Result{Error: err, Status: status, Success: success} + answer := "" + answer = c.getOTP(verifyCtx) + + select { + case <-verifyCtx.Done(): // manual OTP cancelled by push + verify <- Result{Error: nil, Status: "cancelled", Success: false} + default: + cancelPush() + status, success, err := c.verifyOTP(verifyCtx, answer) + verify <- Result{Error: err, Status: status, Success: success} + } }() - select { - case res := <-verify: // manual OTP - fmt.Fprint(c.ReadWriter.Out, res.Status) - case res := <-pushauth: // push - fmt.Fprint(c.ReadWriter.Out, res.Status) - case <-timeoutCtx.Done(): // push timed out - fmt.Fprint(c.ReadWriter.Out, "OTP verification timed out") + for { + select { + case res := <-verify: // manual OTP + if res.Status == "cancelled" { + // verify cancelled; don't print anything + } else if res.Status == "" { + // channel closed; don't print anything + } else { + fmt.Fprint(c.ReadWriter.Out, res.Status) + return nil + } + case res := <-pushauth: // push + if res.Status == "cancelled" { + // push cancelled; don't print anything + } else 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 + } } return nil -- GitLab From f18d91511ab0107ac1060391f96f7a513ca7fb54 Mon Sep 17 00:00:00 2001 From: kmcknight <kmcknight@gitlab.com> Date: Fri, 21 Jan 2022 17:33:40 -0800 Subject: [PATCH 08/25] Passing pushauth flag as bool in json body to internal api doesn't become a bool in ruby; converting to string fixes it --- internal/gitlabnet/twofactorverify/client.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/internal/gitlabnet/twofactorverify/client.go b/internal/gitlabnet/twofactorverify/client.go index cf22cf8f..b62396c7 100644 --- a/internal/gitlabnet/twofactorverify/client.go +++ b/internal/gitlabnet/twofactorverify/client.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/http" + "strconv" "gitlab.com/gitlab-org/gitlab-shell/client" "gitlab.com/gitlab-org/gitlab-shell/internal/command/commandargs" @@ -26,7 +27,7 @@ type RequestBody struct { KeyId string `json:"key_id,omitempty"` UserId int64 `json:"user_id,omitempty"` OTPAttempt string `json:"otp_attempt"` - PushAuth bool `json:"push_auth"` + PushAuth string `json:"push_auth"` } func NewClient(config *config.Config) (*Client, error) { @@ -91,7 +92,7 @@ func (c *Client) getRequestBody(ctx context.Context, args *commandargs.Shell, ot var requestBody *RequestBody if args.GitlabKeyId != "" { - requestBody = &RequestBody{KeyId: args.GitlabKeyId, OTPAttempt: otp, PushAuth: pushauth} + requestBody = &RequestBody{KeyId: args.GitlabKeyId, OTPAttempt: otp, PushAuth: strconv.FormatBool(pushauth)} } else { userInfo, err := client.GetByCommandArgs(ctx, args) @@ -99,7 +100,7 @@ func (c *Client) getRequestBody(ctx context.Context, args *commandargs.Shell, ot return nil, err } - requestBody = &RequestBody{UserId: userInfo.UserId, OTPAttempt: otp, PushAuth: pushauth} + requestBody = &RequestBody{UserId: userInfo.UserId, OTPAttempt: otp, PushAuth: strconv.FormatBool(pushauth)} } return requestBody, nil -- GitLab From 054bf6513f9559b07bb70448c496734e61f39d85 Mon Sep 17 00:00:00 2001 From: kmcknight <kmcknight@gitlab.com> Date: Wed, 26 Jan 2022 13:05:00 -0800 Subject: [PATCH 09/25] Fix ruby test script to match updates to code --- spec/gitlab_shell_two_factor_verify_spec.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/spec/gitlab_shell_two_factor_verify_spec.rb b/spec/gitlab_shell_two_factor_verify_spec.rb index 244d7d85..a90df1d5 100644 --- a/spec/gitlab_shell_two_factor_verify_spec.rb +++ b/spec/gitlab_shell_two_factor_verify_spec.rb @@ -24,13 +24,13 @@ describe 'bin/gitlab-shell 2fa_verify' do key_id = params['key_id'] || params['user_id'].to_s if key_id == '100' - if params['push_auth'] + if params['push_auth'] == "true" res.body = { success: false }.to_json else res.body = { success: true }.to_json end elsif key_id == '102' - if params['push_auth'] + if params['push_auth'] == "true" res.body = { success: true }.to_json end else @@ -99,8 +99,6 @@ describe 'bin/gitlab-shell 2fa_verify' do Open3.popen2(env, cmd) do |stdin, stdout| expect(stdout.gets(5)).to eq('OTP: ') - stdin.puts('123456') - expect(stdout.flush.read).to eq("\nPush OTP validation successful. Git operations are now allowed.\n") end end -- GitLab From 9d514d478f6faeadf8e6c4e7162dd1fe37f19b4d Mon Sep 17 00:00:00 2001 From: kmcknight <kmcknight@gitlab.com> Date: Thu, 27 Jan 2022 16:57:44 -0800 Subject: [PATCH 10/25] Rework tests after code mods --- .../twofactorverify/twofactorverify_test.go | 43 ++++++------------- 1 file changed, 14 insertions(+), 29 deletions(-) diff --git a/internal/command/twofactorverify/twofactorverify_test.go b/internal/command/twofactorverify/twofactorverify_test.go index 2ee02265..3177fc56 100644 --- a/internal/command/twofactorverify/twofactorverify_test.go +++ b/internal/command/twofactorverify/twofactorverify_test.go @@ -7,7 +7,6 @@ import ( "io/ioutil" "net/http" "testing" - "time" "github.com/stretchr/testify/require" @@ -34,24 +33,8 @@ func setup(t *testing.T) []testserver.TestRequestHandler { var body map[string]interface{} switch requestBody.KeyId { case "1": - if requestBody.PushAuth { - body = map[string]interface{}{ - "success": false, - } - } else { - body = map[string]interface{}{ - "success": true, - } - } - json.NewEncoder(w).Encode(body) - case "2": - if requestBody.PushAuth { - body = map[string]interface{}{ - "success": true, - } - } else { - // Stall verifyOTP long enough for pushAuth to complete - time.Sleep(10 * time.Second) + body = map[string]interface{}{ + "success": true, } json.NewEncoder(w).Encode(body) case "error": @@ -88,18 +71,16 @@ func TestExecute(t *testing.T) { expectedOutput string }{ { - desc: "When push is provided", - arguments: &commandargs.Shell{GitlabKeyId: "2"}, - answer: "", - expectedOutput: question + - "Push OTP validation successful. Git operations are now allowed.\n", + desc: "When push is provided", + arguments: &commandargs.Shell{GitlabKeyId: "1"}, + answer: "", + expectedOutput: question + "Push OTP validation successful. Git operations are now allowed.\n", }, { - desc: "With a known key id", - arguments: &commandargs.Shell{GitlabKeyId: "1"}, - answer: "123456\n", - expectedOutput: question + - "OTP validation successful. Git operations are now allowed.\n", + desc: "With a known key id", + arguments: &commandargs.Shell{GitlabKeyId: "1"}, + answer: "123456\n", + expectedOutput: question + "OTP validation successful. Git operations are now allowed.\n", }, { desc: "With bad response", @@ -132,6 +113,10 @@ func TestExecute(t *testing.T) { output := &bytes.Buffer{} input := bytes.NewBufferString(tc.answer) + if tc.answer == "" { // pushauth test must not provide input otherwise it gets treated as a manual OTP + input.Reset() + } + cmd := &Command{ Config: &config.Config{GitlabUrl: url}, Args: tc.arguments, -- GitLab From 2e2d3176bf91046ed296bf5ffbcad59a0bb0f80a Mon Sep 17 00:00:00 2001 From: kmcknight <kmcknight@gitlab.com> Date: Wed, 9 Feb 2022 11:52:55 -0800 Subject: [PATCH 11/25] Remove requirement to pass push_auth flag via internal API body. Also split internal OTP validate API into two separate calls: one for manual OTP, the other for push --- internal/gitlabnet/twofactorverify/client.go | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/internal/gitlabnet/twofactorverify/client.go b/internal/gitlabnet/twofactorverify/client.go index b62396c7..c9c13c8c 100644 --- a/internal/gitlabnet/twofactorverify/client.go +++ b/internal/gitlabnet/twofactorverify/client.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "net/http" - "strconv" "gitlab.com/gitlab-org/gitlab-shell/client" "gitlab.com/gitlab-org/gitlab-shell/internal/command/commandargs" @@ -27,7 +26,6 @@ type RequestBody struct { KeyId string `json:"key_id,omitempty"` UserId int64 `json:"user_id,omitempty"` OTPAttempt string `json:"otp_attempt"` - PushAuth string `json:"push_auth"` } func NewClient(config *config.Config) (*Client, error) { @@ -40,12 +38,12 @@ func NewClient(config *config.Config) (*Client, error) { } func (c *Client) VerifyOTP(ctx context.Context, args *commandargs.Shell, otp string) (bool, string, error) { - requestBody, err := c.getRequestBody(ctx, args, otp, false) + requestBody, err := c.getRequestBody(ctx, args, otp) if err != nil { return false, "", err } - response, err := c.client.Post(ctx, "/two_factor_otp_check", requestBody) + response, err := c.client.Post(ctx, "/two_factor_manual_otp_check", requestBody) if err != nil { return false, "", err } @@ -56,12 +54,12 @@ func (c *Client) VerifyOTP(ctx context.Context, args *commandargs.Shell, otp str func (c *Client) PushAuth(ctx context.Context, args *commandargs.Shell) (bool, string, error) { // enable push auth in internal rest api - requestBody, err := c.getRequestBody(ctx, args, "", true) + requestBody, err := c.getRequestBody(ctx, args, "") if err != nil { return false, "", err } - response, err := c.client.Post(ctx, "/two_factor_otp_check", requestBody) + response, err := c.client.Post(ctx, "/two_factor_push_otp_check", requestBody) if err != nil { return false, "", err } @@ -83,7 +81,7 @@ func parse(hr *http.Response) (bool, string, error) { return true, response.Message, nil } -func (c *Client) getRequestBody(ctx context.Context, args *commandargs.Shell, otp string, pushauth bool) (*RequestBody, error) { +func (c *Client) getRequestBody(ctx context.Context, args *commandargs.Shell, otp string) (*RequestBody, error) { client, err := discover.NewClient(c.config) if err != nil { @@ -92,7 +90,7 @@ func (c *Client) getRequestBody(ctx context.Context, args *commandargs.Shell, ot var requestBody *RequestBody if args.GitlabKeyId != "" { - requestBody = &RequestBody{KeyId: args.GitlabKeyId, OTPAttempt: otp, PushAuth: strconv.FormatBool(pushauth)} + requestBody = &RequestBody{KeyId: args.GitlabKeyId, OTPAttempt: otp} } else { userInfo, err := client.GetByCommandArgs(ctx, args) @@ -100,7 +98,7 @@ func (c *Client) getRequestBody(ctx context.Context, args *commandargs.Shell, ot return nil, err } - requestBody = &RequestBody{UserId: userInfo.UserId, OTPAttempt: otp, PushAuth: strconv.FormatBool(pushauth)} + requestBody = &RequestBody{UserId: userInfo.UserId, OTPAttempt: otp} } return requestBody, nil -- GitLab From d109af093cac2adc78937ef078c1532073153161 Mon Sep 17 00:00:00 2001 From: kmcknight <kmcknight@gitlab.com> Date: Thu, 7 Apr 2022 10:23:13 -0700 Subject: [PATCH 12/25] Fix format errors --- internal/command/twofactorverify/twofactorverify.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/command/twofactorverify/twofactorverify.go b/internal/command/twofactorverify/twofactorverify.go index ecda5c06..e12a1bd2 100644 --- a/internal/command/twofactorverify/twofactorverify.go +++ b/internal/command/twofactorverify/twofactorverify.go @@ -40,10 +40,10 @@ func (c *Command) Execute(ctx context.Context) error { return err } - // Create timeout context + // Create timeout context // TODO: make timeout configurable const ctxTimeout = 30 - timeoutCtx, cancelTimeout := context.WithTimeout(ctx, ctxTimeout * time.Second) + timeoutCtx, cancelTimeout := context.WithTimeout(ctx, ctxTimeout*time.Second) verifyCtx, cancelVerify := context.WithCancel(timeoutCtx) pushCtx, cancelPush := context.WithCancel(timeoutCtx) defer cancelTimeout() -- GitLab From 7ab82c000c5989a4a08015c21df6f3a6ad57afdc Mon Sep 17 00:00:00 2001 From: kmcknight <kmcknight@gitlab.com> Date: Tue, 12 Apr 2022 12:31:37 -0700 Subject: [PATCH 13/25] Split rspec into separate services for push and manual --- ...ab_shell_two_factor_manual_verify_spec.rb} | 30 +------- ...itlab_shell_two_factor_push_verify_spec.rb | 71 +++++++++++++++++++ 2 files changed, 74 insertions(+), 27 deletions(-) rename spec/{gitlab_shell_two_factor_verify_spec.rb => gitlab_shell_two_factor_manual_verify_spec.rb} (68%) create mode 100644 spec/gitlab_shell_two_factor_push_verify_spec.rb diff --git a/spec/gitlab_shell_two_factor_verify_spec.rb b/spec/gitlab_shell_two_factor_manual_verify_spec.rb similarity index 68% rename from spec/gitlab_shell_two_factor_verify_spec.rb rename to spec/gitlab_shell_two_factor_manual_verify_spec.rb index a90df1d5..5e5a06c5 100644 --- a/spec/gitlab_shell_two_factor_verify_spec.rb +++ b/spec/gitlab_shell_two_factor_manual_verify_spec.rb @@ -3,7 +3,7 @@ require_relative 'spec_helper' require 'open3' require 'json' -describe 'bin/gitlab-shell 2fa_verify' do +describe 'bin/gitlab-shell 2fa_verify manual' do include_context 'gitlab shell' let(:env) do @@ -16,7 +16,7 @@ describe 'bin/gitlab-shell 2fa_verify' do end def mock_server(server) - server.mount_proc('/api/v4/internal/two_factor_otp_check') do |req, res| + server.mount_proc('/api/v4/internal/two_factor_manual_otp_check') do |req, res| res.content_type = 'application/json' res.status = 200 @@ -24,15 +24,7 @@ describe 'bin/gitlab-shell 2fa_verify' do key_id = params['key_id'] || params['user_id'].to_s if key_id == '100' - if params['push_auth'] == "true" - res.body = { success: false }.to_json - else - res.body = { success: true }.to_json - end - elsif key_id == '102' - if params['push_auth'] == "true" - res.body = { success: true }.to_json - end + res.body = { success: true }.to_json else res.body = { success: false, message: 'boom!' }.to_json end @@ -46,14 +38,6 @@ describe 'bin/gitlab-shell 2fa_verify' do end describe 'command' do - context 'when push is provided' do - let(:cmd) { "#{gitlab_shell_path} key-102" } - - it 'prints a successful push verification message' do - verify_successful_verification_push!(cmd) - end - end - context 'when key is provided' do let(:cmd) { "#{gitlab_shell_path} key-100" } @@ -94,12 +78,4 @@ describe 'bin/gitlab-shell 2fa_verify' do expect(stdout.flush.read).to eq("\nOTP validation successful. Git operations are now allowed.\n") end end - - def verify_successful_verification_push!(cmd) - Open3.popen2(env, cmd) do |stdin, stdout| - expect(stdout.gets(5)).to eq('OTP: ') - - expect(stdout.flush.read).to eq("\nPush OTP validation successful. Git operations are now allowed.\n") - end - end end diff --git a/spec/gitlab_shell_two_factor_push_verify_spec.rb b/spec/gitlab_shell_two_factor_push_verify_spec.rb new file mode 100644 index 00000000..e8705805 --- /dev/null +++ b/spec/gitlab_shell_two_factor_push_verify_spec.rb @@ -0,0 +1,71 @@ +require_relative 'spec_helper' + +require 'open3' +require 'json' + +describe 'bin/gitlab-shell 2fa_verify push' do + include_context 'gitlab shell' + + let(:env) do + { 'SSH_CONNECTION' => 'fake', + 'SSH_ORIGINAL_COMMAND' => '2fa_verify' } + end + + before(:context) do + write_config('gitlab_url' => "http+unix://#{CGI.escape(tmp_socket_path)}") + end + + def mock_server(server) + server.mount_proc('/api/v4/internal/two_factor_push_otp_check') do |req, res| + res.content_type = 'application/json' + res.status = 200 + + params = JSON.parse(req.body) + key_id = params['key_id'] || params['user_id'].to_s + + if key_id == '100' + res.body = { success: false }.to_json + elsif key_id == '102' + res.body = { success: true }.to_json + else + res.body = { success: false, message: 'boom!' }.to_json + end + end + + server.mount_proc('/api/v4/internal/discover') do |_, res| + res.status = 200 + res.content_type = 'application/json' + res.body = { id: 100, name: 'Some User', username: 'someuser' }.to_json + end + end + + describe 'command' do + context 'when push is provided' do + let(:cmd) { "#{gitlab_shell_path} key-102" } + + it 'prints a successful push verification message' do + verify_successful_verification_push!(cmd) + end + end + + context 'when API error occurs' do + let(:cmd) { "#{gitlab_shell_path} key-101" } + + it 'prints the error message' do + Open3.popen2(env, cmd) do |stdin, stdout| + expect(stdout.gets(5)).to eq('OTP: ') + + expect(stdout.flush.read).to eq("\nPush OTP validation failed.\nboom!\n") + end + end + end + end + + def verify_successful_verification_push!(cmd) + Open3.popen2(env, cmd) do |stdin, stdout| + expect(stdout.gets(5)).to eq('OTP: ') + + expect(stdout.flush.read).to eq("\nPush OTP validation successful. Git operations are now allowed.\n") + end + end +end -- GitLab From e6c46528958276558379fc2e8c4cccc6c9d5c6b5 Mon Sep 17 00:00:00 2001 From: kmcknight <kmcknight@gitlab.com> Date: Tue, 12 Apr 2022 17:09:37 -0700 Subject: [PATCH 14/25] Split out into manual/push --- ..._test.go => twofactorverifymanual_test.go} | 31 ++--- .../twofactorverifypush_test.go | 124 ++++++++++++++++++ 2 files changed, 135 insertions(+), 20 deletions(-) rename internal/command/twofactorverify/{twofactorverify_test.go => twofactorverifymanual_test.go} (72%) create mode 100644 internal/command/twofactorverify/twofactorverifypush_test.go diff --git a/internal/command/twofactorverify/twofactorverify_test.go b/internal/command/twofactorverify/twofactorverifymanual_test.go similarity index 72% rename from internal/command/twofactorverify/twofactorverify_test.go rename to internal/command/twofactorverify/twofactorverifymanual_test.go index d091e3a5..d8de486d 100644 --- a/internal/command/twofactorverify/twofactorverify_test.go +++ b/internal/command/twofactorverify/twofactorverifymanual_test.go @@ -17,10 +17,10 @@ import ( "gitlab.com/gitlab-org/gitlab-shell/internal/gitlabnet/twofactorverify" ) -func setup(t *testing.T) []testserver.TestRequestHandler { +func setupManual(t *testing.T) []testserver.TestRequestHandler { requests := []testserver.TestRequestHandler{ { - Path: "/api/v4/internal/two_factor_otp_check", + 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() @@ -54,12 +54,12 @@ func setup(t *testing.T) []testserver.TestRequestHandler { } const ( - question = "OTP: \n" - errorHeader = "OTP validation failed.\n" + manualQuestion = "OTP: \n" + manualErrorHeader = "OTP validation failed.\n" ) -func TestExecute(t *testing.T) { - requests := setup(t) +func TestExecuteManual(t *testing.T) { + requests := setupManual(t) url := testserver.StartSocketHttpServer(t, requests) @@ -69,41 +69,35 @@ func TestExecute(t *testing.T) { answer string expectedOutput string }{ - { - desc: "When push is provided", - arguments: &commandargs.Shell{GitlabKeyId: "1"}, - answer: "", - expectedOutput: question + "Push OTP validation successful. Git operations are now allowed.\n", - }, { desc: "With a known key id", arguments: &commandargs.Shell{GitlabKeyId: "1"}, answer: "123456\n", - expectedOutput: question + "OTP validation successful. Git operations are now allowed.\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: question + errorHeader + "Parsing failed\n", + expectedOutput: manualQuestion + manualErrorHeader + "Parsing failed\n", }, { desc: "With API returns an error", arguments: &commandargs.Shell{GitlabKeyId: "error"}, answer: "yes\n", - expectedOutput: question + errorHeader + "error message\n", + expectedOutput: manualQuestion + manualErrorHeader + "error message\n", }, { desc: "With API fails", arguments: &commandargs.Shell{GitlabKeyId: "broken"}, answer: "yes\n", - expectedOutput: question + errorHeader + "Internal API error (500)\n", + expectedOutput: manualQuestion + manualErrorHeader + "Internal API error (500)\n", }, { desc: "With missing arguments", arguments: &commandargs.Shell{}, answer: "yes\n", - expectedOutput: question + errorHeader + "who='' is invalid\n", + expectedOutput: "\nPush " + manualErrorHeader + "who='' is invalid\n" +"OTP: ", }, } @@ -112,9 +106,6 @@ func TestExecute(t *testing.T) { output := &bytes.Buffer{} input := bytes.NewBufferString(tc.answer) - if tc.answer == "" { // pushauth test must not provide input otherwise it gets treated as a manual OTP - input.Reset() - } cmd := &Command{ Config: &config.Config{GitlabUrl: url}, diff --git a/internal/command/twofactorverify/twofactorverifypush_test.go b/internal/command/twofactorverify/twofactorverifypush_test.go new file mode 100644 index 00000000..798ff1ee --- /dev/null +++ b/internal/command/twofactorverify/twofactorverifypush_test.go @@ -0,0 +1,124 @@ +package twofactorverify + +import ( + "bytes" + "context" + "encoding/json" + "io" + "net/http" + "testing" + + "github.com/stretchr/testify/require" + + "gitlab.com/gitlab-org/gitlab-shell/client/testserver" + "gitlab.com/gitlab-org/gitlab-shell/internal/command/commandargs" + "gitlab.com/gitlab-org/gitlab-shell/internal/command/readwriter" + "gitlab.com/gitlab-org/gitlab-shell/internal/config" + "gitlab.com/gitlab-org/gitlab-shell/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) + } + }, + }, + } + + 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 bad response", + arguments: &commandargs.Shell{GitlabKeyId: "-1"}, + answer: "", + expectedOutput: pushQuestion + pushErrorHeader + "Parsing failed\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", + }, + { + desc: "With missing arguments", + arguments: &commandargs.Shell{}, + answer: "", + expectedOutput: pushQuestion + pushErrorHeader + "who='' is invalid", + }, + } + + 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()) + }) + } +} -- GitLab From a77b2926669d02c429d6c56735fb8fd24bf0463a Mon Sep 17 00:00:00 2001 From: kmcknight <kmcknight@gitlab.com> Date: Tue, 12 Apr 2022 17:37:37 -0700 Subject: [PATCH 15/25] Split client tests into manual/push --- .../{client_test.go => clientmanual_test.go} | 26 ++-- .../twofactorverify/clientpush_test.go | 139 ++++++++++++++++++ 2 files changed, 152 insertions(+), 13 deletions(-) rename internal/gitlabnet/twofactorverify/{client_test.go => clientmanual_test.go} (82%) create mode 100644 internal/gitlabnet/twofactorverify/clientpush_test.go diff --git a/internal/gitlabnet/twofactorverify/client_test.go b/internal/gitlabnet/twofactorverify/clientmanual_test.go similarity index 82% rename from internal/gitlabnet/twofactorverify/client_test.go rename to internal/gitlabnet/twofactorverify/clientmanual_test.go index cfaf5e51..0ea4c5ed 100644 --- a/internal/gitlabnet/twofactorverify/client_test.go +++ b/internal/gitlabnet/twofactorverify/clientmanual_test.go @@ -16,10 +16,10 @@ import ( "gitlab.com/gitlab-org/gitlab-shell/internal/config" ) -func initialize(t *testing.T) []testserver.TestRequestHandler { +func initializeManual(t *testing.T) []testserver.TestRequestHandler { requests := []testserver.TestRequestHandler{ { - Path: "/api/v4/internal/two_factor_otp_check", + 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() @@ -78,35 +78,35 @@ func initialize(t *testing.T) []testserver.TestRequestHandler { } const ( - otpAttempt = "123456" + manualOtpAttempt = "123456" ) func TestVerifyOTPByKeyId(t *testing.T) { - client := setup(t) + client := setupManual(t) args := &commandargs.Shell{GitlabKeyId: "0"} - _, _, err := client.VerifyOTP(context.Background(), args, otpAttempt) + _, _, err := client.VerifyOTP(context.Background(), args, manualOtpAttempt) require.NoError(t, err) } func TestVerifyOTPByUsername(t *testing.T) { - client := setup(t) + client := setupManual(t) args := &commandargs.Shell{GitlabUsername: "jane-doe"} - _, _, err := client.VerifyOTP(context.Background(), args, otpAttempt) + _, _, err := client.VerifyOTP(context.Background(), args, manualOtpAttempt) require.NoError(t, err) } func TestErrorMessage(t *testing.T) { - client := setup(t) + client := setupManual(t) args := &commandargs.Shell{GitlabKeyId: "1"} - _, reason, _ := client.VerifyOTP(context.Background(), args, otpAttempt) + _, reason, _ := client.VerifyOTP(context.Background(), args, manualOtpAttempt) require.Equal(t, "error message", reason) } func TestErrorResponses(t *testing.T) { - client := setup(t) + client := setupManual(t) testCases := []struct { desc string @@ -133,15 +133,15 @@ func TestErrorResponses(t *testing.T) { 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) + _, _, err := client.VerifyOTP(context.Background(), args, manualOtpAttempt) require.EqualError(t, err, tc.expectedError) }) } } -func setup(t *testing.T) *Client { - requests := initialize(t) +func setupManual(t *testing.T) *Client { + requests := initializeManual(t) url := testserver.StartSocketHttpServer(t, requests) client, err := NewClient(&config.Config{GitlabUrl: url}) diff --git a/internal/gitlabnet/twofactorverify/clientpush_test.go b/internal/gitlabnet/twofactorverify/clientpush_test.go new file mode 100644 index 00000000..35cef606 --- /dev/null +++ b/internal/gitlabnet/twofactorverify/clientpush_test.go @@ -0,0 +1,139 @@ +package twofactorverify + +import ( + "context" + "encoding/json" + "io" + "net/http" + "testing" + + "gitlab.com/gitlab-org/gitlab-shell/internal/gitlabnet/discover" + + "github.com/stretchr/testify/require" + "gitlab.com/gitlab-org/gitlab-shell/client" + "gitlab.com/gitlab-org/gitlab-shell/client/testserver" + "gitlab.com/gitlab-org/gitlab-shell/internal/command/commandargs" + "gitlab.com/gitlab-org/gitlab-shell/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 +} -- GitLab From 3b62b1d361ec7cb4d9e78140c7cfd5339a8769f2 Mon Sep 17 00:00:00 2001 From: kmcknight <kmcknight@gitlab.com> Date: Tue, 12 Apr 2022 17:38:14 -0700 Subject: [PATCH 16/25] Fix test formatting and inconsistency --- .../command/twofactorverify/twofactorverifymanual_test.go | 2 +- .../command/twofactorverify/twofactorverifypush_test.go | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/internal/command/twofactorverify/twofactorverifymanual_test.go b/internal/command/twofactorverify/twofactorverifymanual_test.go index d8de486d..ea092c8b 100644 --- a/internal/command/twofactorverify/twofactorverifymanual_test.go +++ b/internal/command/twofactorverify/twofactorverifymanual_test.go @@ -97,7 +97,7 @@ func TestExecuteManual(t *testing.T) { desc: "With missing arguments", arguments: &commandargs.Shell{}, answer: "yes\n", - expectedOutput: "\nPush " + manualErrorHeader + "who='' is invalid\n" +"OTP: ", + expectedOutput: "\nPush " + manualErrorHeader + "who='' is invalid\n" + "OTP: ", }, } diff --git a/internal/command/twofactorverify/twofactorverifypush_test.go b/internal/command/twofactorverify/twofactorverifypush_test.go index 798ff1ee..3e76eec5 100644 --- a/internal/command/twofactorverify/twofactorverifypush_test.go +++ b/internal/command/twofactorverify/twofactorverifypush_test.go @@ -93,12 +93,6 @@ func TestExecutePush(t *testing.T) { answer: "", expectedOutput: pushQuestion + pushErrorHeader + "Internal API error (500)\n", }, - { - desc: "With missing arguments", - arguments: &commandargs.Shell{}, - answer: "", - expectedOutput: pushQuestion + pushErrorHeader + "who='' is invalid", - }, } for _, tc := range testCases { -- GitLab From 59c44668ad42d2896eda10e392baf08940937ea5 Mon Sep 17 00:00:00 2001 From: kmcknight <kmcknight@gitlab.com> Date: Thu, 14 Apr 2022 11:04:24 -0700 Subject: [PATCH 17/25] Add mock test server handler for push internal api --- .../twofactorverifymanual_test.go | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/internal/command/twofactorverify/twofactorverifymanual_test.go b/internal/command/twofactorverify/twofactorverifymanual_test.go index ea092c8b..930ac1df 100644 --- a/internal/command/twofactorverify/twofactorverifymanual_test.go +++ b/internal/command/twofactorverify/twofactorverifymanual_test.go @@ -48,6 +48,41 @@ func setupManual(t *testing.T) []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) + default: + body = map[string]interface{}{ + "success": true, + "message": "default message", + } + json.NewEncoder(w).Encode(body) + } + }, + }, } return requests @@ -97,7 +132,7 @@ func TestExecuteManual(t *testing.T) { desc: "With missing arguments", arguments: &commandargs.Shell{}, answer: "yes\n", - expectedOutput: "\nPush " + manualErrorHeader + "who='' is invalid\n" + "OTP: ", + expectedOutput: manualQuestion + manualErrorHeader + "who='' is invalid\n", }, } -- GitLab From bbb1fb56972f4cf51688f662d7f796ad5acf8e64 Mon Sep 17 00:00:00 2001 From: kmcknight <kmcknight@gitlab.com> Date: Thu, 14 Apr 2022 16:52:53 -0700 Subject: [PATCH 18/25] Update test cases --- .../command/twofactorverify/twofactorverifymanual_test.go | 6 ------ .../command/twofactorverify/twofactorverifypush_test.go | 6 ------ 2 files changed, 12 deletions(-) diff --git a/internal/command/twofactorverify/twofactorverifymanual_test.go b/internal/command/twofactorverify/twofactorverifymanual_test.go index 930ac1df..c914e778 100644 --- a/internal/command/twofactorverify/twofactorverifymanual_test.go +++ b/internal/command/twofactorverify/twofactorverifymanual_test.go @@ -128,12 +128,6 @@ func TestExecuteManual(t *testing.T) { answer: "yes\n", expectedOutput: manualQuestion + manualErrorHeader + "Internal API error (500)\n", }, - { - desc: "With missing arguments", - arguments: &commandargs.Shell{}, - answer: "yes\n", - expectedOutput: manualQuestion + manualErrorHeader + "who='' is invalid\n", - }, } for _, tc := range testCases { diff --git a/internal/command/twofactorverify/twofactorverifypush_test.go b/internal/command/twofactorverify/twofactorverifypush_test.go index 3e76eec5..7bd5b61e 100644 --- a/internal/command/twofactorverify/twofactorverifypush_test.go +++ b/internal/command/twofactorverify/twofactorverifypush_test.go @@ -75,12 +75,6 @@ func TestExecutePush(t *testing.T) { answer: "", expectedOutput: pushQuestion + "Push OTP validation successful. Git operations are now allowed.\n", }, - { - desc: "With bad response", - arguments: &commandargs.Shell{GitlabKeyId: "-1"}, - answer: "", - expectedOutput: pushQuestion + pushErrorHeader + "Parsing failed\n", - }, { desc: "With API returns an error", arguments: &commandargs.Shell{GitlabKeyId: "error"}, -- GitLab From aac413ea1e1a7074b2c52431b67df215e2c929ba Mon Sep 17 00:00:00 2001 From: kmcknight <kmcknight@gitlab.com> Date: Tue, 12 Jul 2022 17:11:24 -0700 Subject: [PATCH 19/25] Remove redundant contexts --- .../command/twofactorverify/twofactorverify.go | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/internal/command/twofactorverify/twofactorverify.go b/internal/command/twofactorverify/twofactorverify.go index e12a1bd2..c9c0462d 100644 --- a/internal/command/twofactorverify/twofactorverify.go +++ b/internal/command/twofactorverify/twofactorverify.go @@ -44,22 +44,20 @@ func (c *Command) Execute(ctx context.Context) error { // TODO: make timeout configurable const ctxTimeout = 30 timeoutCtx, cancelTimeout := context.WithTimeout(ctx, ctxTimeout*time.Second) - verifyCtx, cancelVerify := context.WithCancel(timeoutCtx) - pushCtx, cancelPush := context.WithCancel(timeoutCtx) defer cancelTimeout() // Background push notification with timeout pushauth := make(chan Result) go func() { defer close(pushauth) - status, success, err := c.pushAuth(pushCtx) + status, success, err := c.pushAuth(timeoutCtx) select { - case <-pushCtx.Done(): // push cancelled by manual OTP + case <-timeoutCtx.Done(): // push cancelled by manual OTP pushauth <- Result{Error: nil, Status: "cancelled", Success: false} default: pushauth <- Result{Error: err, Status: status, Success: success} - cancelVerify() + cancelTimeout() } }() @@ -69,15 +67,15 @@ func (c *Command) Execute(ctx context.Context) error { defer close(verify) ctxlog.Info("twofactorverify: execute: waiting for user input") answer := "" - answer = c.getOTP(verifyCtx) + answer = c.getOTP(timeoutCtx) select { - case <-verifyCtx.Done(): // manual OTP cancelled by push + case <-timeoutCtx.Done(): // manual OTP cancelled by push verify <- Result{Error: nil, Status: "cancelled", Success: false} default: - cancelPush() + cancelTimeout() ctxlog.Info("twofactorverify: execute: verifying entered OTP") - status, success, err := c.verifyOTP(verifyCtx, answer) + status, success, err := c.verifyOTP(timeoutCtx, answer) ctxlog.WithError(err).Info("twofactorverify: execute: OTP verified") verify <- Result{Error: err, Status: status, Success: success} } -- GitLab From 9b2246d8daa9f8b70dd5e8ed92179c6593b8708c Mon Sep 17 00:00:00 2001 From: kmcknight <kmcknight@gitlab.com> Date: Tue, 12 Jul 2022 18:03:09 -0700 Subject: [PATCH 20/25] Remove redundant channel --- .../twofactorverify/twofactorverify.go | 30 +++++++------------ 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/internal/command/twofactorverify/twofactorverify.go b/internal/command/twofactorverify/twofactorverify.go index c9c0462d..bc7123b9 100644 --- a/internal/command/twofactorverify/twofactorverify.go +++ b/internal/command/twofactorverify/twofactorverify.go @@ -46,55 +46,47 @@ func (c *Command) Execute(ctx context.Context) error { timeoutCtx, cancelTimeout := context.WithTimeout(ctx, ctxTimeout*time.Second) defer cancelTimeout() + // Create result channel + resultC := make(chan Result) + // Background push notification with timeout - pushauth := make(chan Result) go func() { - defer close(pushauth) + defer close(resultC) status, success, err := c.pushAuth(timeoutCtx) select { case <-timeoutCtx.Done(): // push cancelled by manual OTP - pushauth <- Result{Error: nil, Status: "cancelled", Success: false} + resultC <- Result{Error: nil, Status: "cancelled", Success: false} default: - pushauth <- Result{Error: err, Status: status, Success: success} + resultC <- Result{Error: err, Status: status, Success: success} cancelTimeout() } }() // Also allow manual OTP entry while waiting for push, with same timeout as push - verify := make(chan Result) go func() { - defer close(verify) + defer close(resultC) ctxlog.Info("twofactorverify: execute: waiting for user input") answer := "" answer = c.getOTP(timeoutCtx) select { case <-timeoutCtx.Done(): // manual OTP cancelled by push - verify <- Result{Error: nil, Status: "cancelled", Success: false} + resultC <- Result{Error: nil, Status: "cancelled", Success: false} default: cancelTimeout() 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} + resultC <- Result{Error: err, Status: status, Success: success} } }() for { select { - case res := <-verify: // manual OTP - if res.Status == "cancelled" { - // verify cancelled; don't print anything - } else if res.Status == "" { - // channel closed; don't print anything - } else { - fmt.Fprint(c.ReadWriter.Out, res.Status) - return nil - } - case res := <-pushauth: // push + case res := <-resultC: if res.Status == "cancelled" { - // push cancelled; don't print anything + // request cancelled; don't print anything } else if res.Status == "" { // channel closed; don't print anything } else { -- GitLab From ac9d9bd756b0b215ea7e2519a3748eec8124d91b Mon Sep 17 00:00:00 2001 From: James Sandlin <jsandlin@gitlab.com> Date: Fri, 15 Jul 2022 09:01:18 -0700 Subject: [PATCH 21/25] reorg --- .../twofactorverify/twofactorverify.go | 215 ++++++++++++++++-- .../twofactorverifymanual_test.go | 36 +-- 2 files changed, 212 insertions(+), 39 deletions(-) diff --git a/internal/command/twofactorverify/twofactorverify.go b/internal/command/twofactorverify/twofactorverify.go index bc7123b9..26daedcc 100644 --- a/internal/command/twofactorverify/twofactorverify.go +++ b/internal/command/twofactorverify/twofactorverify.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "io" + "sync" "time" "gitlab.com/gitlab-org/labkit/log" @@ -27,7 +28,13 @@ type Result struct { Success bool } -func (c *Command) Execute(ctx context.Context) error { +var ( + mu sync.RWMutex + // TODO: make timeout configurable + ctxMaxTime = time.Second + 30 +) + +func (c *Command) Execute1(ctx context.Context) error { ctxlog := log.ContextLogger(ctx) // config.GetHTTPClient isn't thread-safe so save Client in struct for concurrency @@ -44,49 +51,71 @@ func (c *Command) Execute(ctx context.Context) error { // TODO: make timeout configurable const ctxTimeout = 30 timeoutCtx, cancelTimeout := context.WithTimeout(ctx, ctxTimeout*time.Second) + verifyCtx, cancelVerify := context.WithCancel(timeoutCtx) + pushCtx, cancelPush := context.WithCancel(timeoutCtx) defer cancelTimeout() - // Create result channel - resultC := make(chan Result) - // Background push notification with timeout + pushauth := make(chan Result) go func() { - defer close(resultC) - status, success, err := c.pushAuth(timeoutCtx) + defer close(pushauth) + status, success, err := c.pushAuth(pushCtx) select { - case <-timeoutCtx.Done(): // push cancelled by manual OTP - resultC <- Result{Error: nil, Status: "cancelled", Success: false} + case <-pushCtx.Done(): // push cancelled by manual OTP + pushauth <- Result{Error: nil, Status: "cancelled", Success: false} default: - resultC <- Result{Error: err, Status: status, Success: success} - cancelTimeout() + pushauth <- Result{Error: err, Status: status, Success: success} + cancelVerify() } }() // Also allow manual OTP entry while waiting for push, with same timeout as push + verify := make(chan Result) go func() { - defer close(resultC) + defer close(verify) ctxlog.Info("twofactorverify: execute: waiting for user input") answer := "" - answer = c.getOTP(timeoutCtx) + answer = c.getOTP(verifyCtx) select { - case <-timeoutCtx.Done(): // manual OTP cancelled by push - resultC <- Result{Error: nil, Status: "cancelled", Success: false} + case <-verifyCtx.Done(): // manual OTP cancelled by push + verify <- Result{Error: nil, Status: "cancelled", Success: false} default: - cancelTimeout() + cancelPush() ctxlog.Info("twofactorverify: execute: verifying entered OTP") - status, success, err := c.verifyOTP(timeoutCtx, answer) + status, success, err := c.verifyOTP(verifyCtx, answer) + fmt.Println("-------------") + fmt.Println("pushAuth.status = ", status) + fmt.Println("pushAuth.success = ", success) + fmt.Println("pushAuth.err = ", err) + fmt.Println("-------------") ctxlog.WithError(err).Info("twofactorverify: execute: OTP verified") - resultC <- Result{Error: err, Status: status, Success: success} + verify <- Result{Error: err, Status: status, Success: success} } }() + for { select { - case res := <-resultC: + case res := <-verify: // manual OTP + fmt.Println("-------------") + fmt.Println("verify.res = ", res) + fmt.Println("-------------") if res.Status == "cancelled" { - // request cancelled; don't print anything + // verify cancelled; don't print anything + } else if res.Status == "" { + // channel closed; don't print anything + } else { + fmt.Fprint(c.ReadWriter.Out, res.Status) + return nil + } + case res := <-pushauth: // push + fmt.Println("-------------") + fmt.Println("pushauth.res = ", res) + fmt.Println("-------------") + if res.Status == "cancelled" { + // push cancelled; don't print anything } else if res.Status == "" { // channel closed; don't print anything } else { @@ -101,6 +130,70 @@ func (c *Command) Execute(ctx context.Context) error { return nil } +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) + + if err != nil { + ctxlog.WithError(err).Error("twofactorverify: execute: OTP verification failed") + return err + } + + // Create timeout context + // TODO: make timeout configurable + const ctxTimeout = 30 + myctx, mycancel := context.WithCancel(ctx) + defer mycancel() + //defer cancelTimeout() + // + //// Create result channel + //resultC := make(chan Result) + + c.processCmd(myctx, mycancel) + + // + for { + fmt.Println("for") + fmt.Println(myctx) + select { + case <- myctx.Done(): + fmt.Println("myctx.Done") + //case resPush := <-pushAuth.D: + // fmt.Println("resPush => ", resPush.Status) + // //if resPush.Status == "cancelled" { + // // // request cancelled; don't print anything + // //} else if resPush.Status == "" { + // // // channel closed; don't print anything + // //} else { + // // fmt.Fprint(c.ReadWriter.Out, resPush.Status) + // // return nil + // //} + //case resOtp := <-otpAuth: + // fmt.Println("otpAuth => ", resOtp) + // //if resOtp.Status == "cancelled" { + // // // request cancelled; don't print anything + // //} else if resOtp.Status == "" { + // // // channel closed; don't print anything + // //} else { + // // fmt.Fprint(c.ReadWriter.Out, resOtp.Status) + // // return nil + // //} + + case <-myctx.Done(): // push timed out + fmt.Fprint(c.ReadWriter.Out, "\nOTP verification timed out\n") + return nil + default: + fmt.Println("myctx == ", myctx) + + } + } + + return nil +} func (c *Command) getOTP(ctx context.Context) string { prompt := "OTP: " @@ -116,9 +209,84 @@ func (c *Command) getOTP(ctx context.Context) string { return answer } +func (c *Command) processCmd(ctx context.Context, cancelTimeout context.CancelFunc) (status string, success bool, err error) { + ctxlog := log.ContextLogger(ctx) + // Background push notification with timeout + pushAuth := make(chan Result) + go func() { + defer close(pushAuth) + status, success, err := c.pushAuth(ctx) + fmt.Println("-------------") + fmt.Println("pushAuth.status = ", status) + fmt.Println("pushAuth.success = ", success) + fmt.Println("pushAuth.err = ", err) + fmt.Println("-------------") + + select { + case <-ctx.Done(): // push cancelled by manual OTP + fmt.Println("pushAuth.func.timeoutCtx.Done()") + pushAuth <- Result{Error: nil, Status: "cancelled", Success: false} + default: + fmt.Println("pushAuth.func.default") + pushAuth <- Result{Error: err, Status: status, Success: success} + cancelTimeout() + } + }() + + // Also allow manual OTP entry while waiting for push, with same timeout as push + otpAuth := make(chan Result) + go func() { + defer close(otpAuth) + fmt.Println("twofactorverify: execute: waiting for user input") + otpAnswer := c.getOTP(ctx) + fmt.Println("otpAnswer = ", otpAnswer) + + select { + case <-ctx.Done(): // manual OTP cancelled by push + fmt.Println("otpAuth.func.timeoutCtx.Done()") + otpAuth <- Result{Error: nil, Status: "cancelled", Success: false} + default: + fmt.Println("otpAuth.func.timeoutCtx.default") + cancelTimeout() + fmt.Println("twofactorverify: execute: verifying entered OTP") + status, success, err := c.verifyOTP(ctx, otpAnswer) + ctxlog.WithError(err).Info("twofactorverify: execute: OTP verified") + otpAuth <- Result{Error: err, Status: status, Success: success} + } + }() + for { + select { + case pres := <- pushAuth: + fmt.Println("-------------") + fmt.Println("pushAuth = ", pres) + fmt.Println("-------------") + if len(pres.Status) > 0 { + fmt.Println("-------------") + fmt.Println("pushAuth = ", pres.Status) + fmt.Println("-------------") + } + case ores := <- otpAuth: + fmt.Println("-------------") + fmt.Println("otpAuth = ", ores) + fmt.Println("-------------") + if len(ores.Status) > 0 { + fmt.Println("-------------") + fmt.Println("otpAuth = ", ores.Status) + fmt.Println("-------------") + + } + } + } + return +} + + + + + func (c *Command) verifyOTP(ctx context.Context, otp string) (status string, success bool, err error) { reason := "" - + fmt.Println("verifyOTP(ctx, ",otp,")") success, reason, err = c.Client.VerifyOTP(ctx, c.Args, otp) if success { status = fmt.Sprintf("\nOTP validation successful. Git operations are now allowed.\n") @@ -136,9 +304,14 @@ func (c *Command) verifyOTP(ctx context.Context, otp string) (status string, suc } func (c *Command) pushAuth(ctx context.Context) (status string, success bool, err error) { + fmt.Println("---------------------------------------") reason := "" - + fmt.Println(c.Args) success, reason, err = c.Client.PushAuth(ctx, c.Args) + fmt.Println("pushAuth.reason = ", reason) + fmt.Println("pushAuth.success = ", success) + fmt.Println("pushAuth.err = ", err) + fmt.Println("---------------------------------------") if success { status = fmt.Sprintf("\nPush OTP validation successful. Git operations are now allowed.\n") } else { diff --git a/internal/command/twofactorverify/twofactorverifymanual_test.go b/internal/command/twofactorverify/twofactorverifymanual_test.go index c914e778..71372798 100644 --- a/internal/command/twofactorverify/twofactorverifymanual_test.go +++ b/internal/command/twofactorverify/twofactorverifymanual_test.go @@ -110,24 +110,24 @@ func TestExecuteManual(t *testing.T) { 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", - }, + //{ + // 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 { -- GitLab From d791ce333dd072c181cb519163e26eda4ea7e7cc Mon Sep 17 00:00:00 2001 From: James Sandlin <jsandlin@gitlab.com> Date: Fri, 15 Jul 2022 09:03:44 -0700 Subject: [PATCH 22/25] add temp files --- .../twofactorverify/twofactorverify.go.james | 233 ++++++++++++++++++ .../twofactorverify/twofactorverify.go.new | 157 ++++++++++++ .../twofactorverify/twofactorverify.go.old | 163 ++++++++++++ 3 files changed, 553 insertions(+) create mode 100644 internal/command/twofactorverify/twofactorverify.go.james create mode 100644 internal/command/twofactorverify/twofactorverify.go.new create mode 100644 internal/command/twofactorverify/twofactorverify.go.old diff --git a/internal/command/twofactorverify/twofactorverify.go.james b/internal/command/twofactorverify/twofactorverify.go.james new file mode 100644 index 00000000..8ba501aa --- /dev/null +++ b/internal/command/twofactorverify/twofactorverify.go.james @@ -0,0 +1,233 @@ +package twofactorverify + +import ( + "context" + "fmt" + "io" + "sync" + "time" + + "gitlab.com/gitlab-org/labkit/log" + + "gitlab.com/gitlab-org/gitlab-shell/internal/command/commandargs" + "gitlab.com/gitlab-org/gitlab-shell/internal/command/readwriter" + "gitlab.com/gitlab-org/gitlab-shell/internal/config" + "gitlab.com/gitlab-org/gitlab-shell/internal/gitlabnet/twofactorverify" +) + +type Command struct { + Config *config.Config + Client *twofactorverify.Client + Args *commandargs.Shell + ReadWriter *readwriter.ReadWriter +} + +type Result struct { + Error error + Status string + Success bool +} + +// A context to be used as a merged context for both push && OTP entry +type waitContext struct { + mu sync.Mutex + mainCtx context.Context + ctx context.Context + done chan struct {} + err error +} + + +var ( + mu sync.RWMutex + // TODO: make timeout configurable + ctxMaxTime = time.Second + 30 +) + +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) + + if err != nil { + ctxlog.WithError(err).Error("twofactorverify: execute: OTP verification failed") + return err + } + //var mainCancel context.CancelFunc + //mainContext, mainCancel = context.WithCancel(ctx) + timeoutCtx, timeoutCancel := context.WithTimeout(ctx, ctxMaxTime) + defer timeoutCancel() + + // Create Result Channel + // It seems to me that if we use the same channel for each request & defer it multiple times things will fail. So I'm not doing that. + //resultChannel := make(chan Result) + + // Time before forced task cancellation + timeoutCh := time.After(ctxMaxTime) + + // Send push Auth Request + pushAuth := make(chan Result) + go func() { + defer close(pushAuth) + status, success, err := c.pushAuth(timeoutCtx) + ctxlog.Info("pushAuth.status = ", status) + ctxlog.Info("pushAuth.success = ", success) + ctxlog.Info("pushAuth.err = ", err) + select { + case <-timeoutCtx.Done(): // push cancelled by manual OTP + resultC <- Result{Error: nil, Status: "cancelled", Success: false} + default: + resultC <- Result{Error: err, Status: status, Success: success} + cancelTimeout() + } + + }() + + // Send OTP Auth Request + otpAuth := make(chan Result) + go func(){ + defer close(otpAuth) + ctxlog.Info("twofactorverify: waiting for user input.") + otpAnswer := c.getOTP(mainCtx) + ctxlog.Info("otpAnswer = ", otpAnswer) + }() + + + + // + //// Wait until tasks completed or time.After event occurred. + select { + case <- tasksCancelled: + ctxlog.Info("case tasksCancelled") + ctxlog.Info(pushAuth) + case <- timeoutCh: + ctxlog.Info("case Timeout") + // Wait until all done. + <-tasksCancelled + } + // + ////timeoutCtx, cancelTimeout := context.WithTimeout(ctx, ctxTimeout) + //context, cancelVerify := context.WithCancel(timeoutCtx) + //pushCtx, cancelPush := context.WithCancel(timeoutCtx) + //defer cancelTimeout() + // + //// Background push notification with timeout + //pushauth := make(chan Result) + //go func() { + // defer close(pushauth) + // status, success, err := c.pushAuth(pushCtx) + // + // select { + // case <-pushCtx.Done(): // push cancelled by manual OTP + // pushauth <- Result{Error: nil, Status: "cancelled", Success: false} + // default: + // pushauth <- Result{Error: err, Status: status, Success: success} + // cancelVerify() + // } + //}() + // + //// 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(verifyCtx) + // + // select { + // case <-verifyCtx.Done(): // manual OTP cancelled by push + // verify <- Result{Error: nil, Status: "cancelled", Success: false} + // default: + // cancelPush() + // ctxlog.Info("twofactorverify: execute: verifying entered OTP") + // status, success, err := c.verifyOTP(verifyCtx, answer) + // ctxlog.WithError(err).Info("twofactorverify: execute: OTP verified") + // verify <- Result{Error: err, Status: status, Success: success} + // } + //}() + // + //for { + // select { + // case res := <-verify: // manual OTP + // if res.Status == "cancelled" { + // // verify cancelled; don't print anything + // } else if res.Status == "" { + // // channel closed; don't print anything + // } else { + // fmt.Fprint(c.ReadWriter.Out, res.Status) + // return nil + // } + // case res := <-pushauth: // push + // if res.Status == "cancelled" { + // // push cancelled; don't print anything + // } else 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 + // } + //} + // + return nil +} + + +func (c *Command) getOTP(ctx context.Context) string { + prompt := "OTP: " + fmt.Fprint(c.ReadWriter.Out, prompt) + + var answer string + otpLength := int64(64) + reader := io.LimitReader(c.ReadWriter.In, otpLength) + if _, err := fmt.Fscanln(reader, &answer); err != nil { + 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) + fmt.Println("success = " , success) + fmt.Println("reason = ", reason) + fmt.Println("err = ", err) + 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) + } + } + + err = nil + + return +} + +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 +} diff --git a/internal/command/twofactorverify/twofactorverify.go.new b/internal/command/twofactorverify/twofactorverify.go.new new file mode 100644 index 00000000..447b663c --- /dev/null +++ b/internal/command/twofactorverify/twofactorverify.go.new @@ -0,0 +1,157 @@ +package twofactorverify + +import ( + "context" + "fmt" + "io" + "time" + + "gitlab.com/gitlab-org/labkit/log" + + "gitlab.com/gitlab-org/gitlab-shell/internal/command/commandargs" + "gitlab.com/gitlab-org/gitlab-shell/internal/command/readwriter" + "gitlab.com/gitlab-org/gitlab-shell/internal/config" + "gitlab.com/gitlab-org/gitlab-shell/internal/gitlabnet/twofactorverify" +) + +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) + + 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() + + // Create result channel + resultC := make(chan Result) + + // Background push notification with timeout + go func() { + defer close(resultC) + status, success, err := c.pushAuth(timeoutCtx) + + select { + case <-timeoutCtx.Done(): // push cancelled by manual OTP + resultC <- Result{Error: nil, Status: "cancelled", Success: false} + default: + resultC <- Result{Error: err, Status: status, Success: success} + cancelTimeout() + } + }() + + // Also allow manual OTP entry while waiting for push, with same timeout as push + go func() { + defer close(resultC) + ctxlog.Info("twofactorverify: execute: waiting for user input") + answer := "" + answer = c.getOTP(timeoutCtx) + + select { + case <-timeoutCtx.Done(): // manual OTP cancelled by push + resultC <- Result{Error: nil, Status: "cancelled", Success: false} + default: + cancelTimeout() + ctxlog.Info("twofactorverify: execute: verifying entered OTP") + status, success, err := c.verifyOTP(timeoutCtx, answer) + ctxlog.WithError(err).Info("twofactorverify: execute: OTP verified") + resultC <- Result{Error: err, Status: status, Success: success} + } + }() + + for { + select { + case res := <-resultC: + if res.Status == "cancelled" { + // request cancelled; don't print anything + } else 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 + } + } + + return nil +} + +func (c *Command) getOTP(ctx context.Context) string { + prompt := "OTP: " + fmt.Fprint(c.ReadWriter.Out, prompt) + + var answer string + otpLength := int64(64) + reader := io.LimitReader(c.ReadWriter.In, otpLength) + if _, err := fmt.Fscanln(reader, &answer); err != nil { + 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) { + fmt.Sprintf("verifyOTP") + reason := "" + success, reason, err = c.Client.VerifyOTP(ctx, c.Args, otp) + + fmt.Sprintf("\nSUCCESS = #{success}\n") + fmt.Sprintf("\nREASON = #{reason}\n") + fmt.Sprintf("\nERR = #{err}\n") + 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) + } + } + + err = nil + + return +} + +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 +} diff --git a/internal/command/twofactorverify/twofactorverify.go.old b/internal/command/twofactorverify/twofactorverify.go.old new file mode 100644 index 00000000..e12a1bd2 --- /dev/null +++ b/internal/command/twofactorverify/twofactorverify.go.old @@ -0,0 +1,163 @@ +package twofactorverify + +import ( + "context" + "fmt" + "io" + "time" + + "gitlab.com/gitlab-org/labkit/log" + + "gitlab.com/gitlab-org/gitlab-shell/internal/command/commandargs" + "gitlab.com/gitlab-org/gitlab-shell/internal/command/readwriter" + "gitlab.com/gitlab-org/gitlab-shell/internal/config" + "gitlab.com/gitlab-org/gitlab-shell/internal/gitlabnet/twofactorverify" +) + +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) + + 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) + verifyCtx, cancelVerify := context.WithCancel(timeoutCtx) + pushCtx, cancelPush := context.WithCancel(timeoutCtx) + defer cancelTimeout() + + // Background push notification with timeout + pushauth := make(chan Result) + go func() { + defer close(pushauth) + status, success, err := c.pushAuth(pushCtx) + + select { + case <-pushCtx.Done(): // push cancelled by manual OTP + pushauth <- Result{Error: nil, Status: "cancelled", Success: false} + default: + pushauth <- Result{Error: err, Status: status, Success: success} + cancelVerify() + } + }() + + // 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(verifyCtx) + + select { + case <-verifyCtx.Done(): // manual OTP cancelled by push + verify <- Result{Error: nil, Status: "cancelled", Success: false} + default: + cancelPush() + ctxlog.Info("twofactorverify: execute: verifying entered OTP") + status, success, err := c.verifyOTP(verifyCtx, answer) + ctxlog.WithError(err).Info("twofactorverify: execute: OTP verified") + verify <- Result{Error: err, Status: status, Success: success} + } + }() + + for { + select { + case res := <-verify: // manual OTP + if res.Status == "cancelled" { + // verify cancelled; don't print anything + } else if res.Status == "" { + // channel closed; don't print anything + } else { + fmt.Fprint(c.ReadWriter.Out, res.Status) + return nil + } + case res := <-pushauth: // push + if res.Status == "cancelled" { + // push cancelled; don't print anything + } else 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 + } + } + + return nil +} + +func (c *Command) getOTP(ctx context.Context) string { + prompt := "OTP: " + fmt.Fprint(c.ReadWriter.Out, prompt) + + var answer string + otpLength := int64(64) + reader := io.LimitReader(c.ReadWriter.In, otpLength) + if _, err := fmt.Fscanln(reader, &answer); err != nil { + 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) + } + } + + err = nil + + return +} + +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 +} -- GitLab From c93f5456da5a0696d974c071d43c5d0faf31bb88 Mon Sep 17 00:00:00 2001 From: James Sandlin <jsandlin@gitlab.com> Date: Fri, 15 Jul 2022 13:38:38 -0700 Subject: [PATCH 23/25] All testing for otpAuth now works. --- .../twofactorverify/twofactorverify.go | 219 ++++++++---------- .../twofactorverifymanual_test.go | 36 +-- internal/gitlabnet/twofactorverify/client.go | 5 + 3 files changed, 116 insertions(+), 144 deletions(-) diff --git a/internal/command/twofactorverify/twofactorverify.go b/internal/command/twofactorverify/twofactorverify.go index 26daedcc..23f3f8bd 100644 --- a/internal/command/twofactorverify/twofactorverify.go +++ b/internal/command/twofactorverify/twofactorverify.go @@ -34,14 +34,17 @@ var ( ctxMaxTime = time.Second + 30 ) -func (c *Command) Execute1(ctx context.Context) error { +func (c *Command) Execute2(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) - + fmt.Println("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx") + fmt.Println("client = ", c.Client) + fmt.Println("err = ", err) + fmt.Println("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx") if err != nil { ctxlog.WithError(err).Error("twofactorverify: execute: OTP verification failed") return err @@ -54,7 +57,9 @@ func (c *Command) Execute1(ctx context.Context) error { verifyCtx, cancelVerify := context.WithCancel(timeoutCtx) pushCtx, cancelPush := context.WithCancel(timeoutCtx) defer cancelTimeout() - + //fmt.Println("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx") + //fmt.Println(verifyCtx, ", ", cancelVerify) + //fmt.Println("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx") // Background push notification with timeout pushauth := make(chan Result) go func() { @@ -85,11 +90,11 @@ func (c *Command) Execute1(ctx context.Context) error { cancelPush() ctxlog.Info("twofactorverify: execute: verifying entered OTP") status, success, err := c.verifyOTP(verifyCtx, answer) - fmt.Println("-------------") - fmt.Println("pushAuth.status = ", status) - fmt.Println("pushAuth.success = ", success) - fmt.Println("pushAuth.err = ", err) - fmt.Println("-------------") + //fmt.Println("-------------") + //fmt.Println("pushAuth.status = ", status) + //fmt.Println("pushAuth.success = ", success) + //fmt.Println("pushAuth.err = ", err) + //fmt.Println("-------------") ctxlog.WithError(err).Info("twofactorverify: execute: OTP verified") verify <- Result{Error: err, Status: status, Success: success} } @@ -99,9 +104,9 @@ func (c *Command) Execute1(ctx context.Context) error { for { select { case res := <-verify: // manual OTP - fmt.Println("-------------") - fmt.Println("verify.res = ", res) - fmt.Println("-------------") + //fmt.Println("-------------") + //fmt.Println("verify.res = ", res) + //fmt.Println("-------------") if res.Status == "cancelled" { // verify cancelled; don't print anything } else if res.Status == "" { @@ -111,9 +116,9 @@ func (c *Command) Execute1(ctx context.Context) error { return nil } case res := <-pushauth: // push - fmt.Println("-------------") - fmt.Println("pushauth.res = ", res) - fmt.Println("-------------") + //fmt.Println("-------------") + //fmt.Println("pushauth.res = ", res) + //fmt.Println("-------------") if res.Status == "cancelled" { // push cancelled; don't print anything } else if res.Status == "" { @@ -144,136 +149,79 @@ func (c *Command) Execute(ctx context.Context) error { } // Create timeout context - // TODO: make timeout configurable - const ctxTimeout = 30 myctx, mycancel := context.WithCancel(ctx) defer mycancel() - //defer cancelTimeout() - // - //// Create result channel - //resultC := make(chan Result) - c.processCmd(myctx, mycancel) + otpAuth := make(chan Result) + go func() { + defer close(otpAuth) + ctxlog.Info("twofactorverify: execute: waiting for user input") + otpAnswer := c.getOTP(myctx) - // + select { + case <-ctx.Done(): // manual OTP cancelled by push + otpAuth <- Result{Error: nil, Status: "cancelled", Success: false} + default: + status, success, err := c.verifyOTP(myctx, otpAnswer) + otpAuth <- Result{Error: err, Status: status, Success: success} + } + }() for { - fmt.Println("for") - fmt.Println(myctx) select { - case <- myctx.Done(): - fmt.Println("myctx.Done") - //case resPush := <-pushAuth.D: - // fmt.Println("resPush => ", resPush.Status) - // //if resPush.Status == "cancelled" { - // // // request cancelled; don't print anything - // //} else if resPush.Status == "" { - // // // channel closed; don't print anything - // //} else { - // // fmt.Fprint(c.ReadWriter.Out, resPush.Status) - // // return nil - // //} - //case resOtp := <-otpAuth: - // fmt.Println("otpAuth => ", resOtp) - // //if resOtp.Status == "cancelled" { - // // // request cancelled; don't print anything - // //} else if resOtp.Status == "" { - // // // channel closed; don't print anything - // //} else { - // // fmt.Fprint(c.ReadWriter.Out, resOtp.Status) - // // return nil - // //} - - case <-myctx.Done(): // push timed out - fmt.Fprint(c.ReadWriter.Out, "\nOTP verification timed out\n") + case res := <- otpAuth: + if len(res.Status) > 0 && res.Status != "cancelled"{ + fmt.Fprint(c.ReadWriter.Out, res.Status) return nil - default: - fmt.Println("myctx == ", myctx) - + } } } return nil } -func (c *Command) getOTP(ctx context.Context) string { - prompt := "OTP: " - fmt.Fprint(c.ReadWriter.Out, prompt) - - var answer string - otpLength := int64(64) - reader := io.LimitReader(c.ReadWriter.In, otpLength) - if _, err := fmt.Fscanln(reader, &answer); err != nil { - log.ContextLogger(ctx).WithError(err).Debug("twofactorverify: getOTP: Failed to get user input") - } - - return answer -} - -func (c *Command) processCmd(ctx context.Context, cancelTimeout context.CancelFunc) (status string, success bool, err error) { +func (c Command) processCmd(ctx context.Context, cancelTimeout context.CancelFunc) (result Result) { ctxlog := log.ContextLogger(ctx) - // Background push notification with timeout - pushAuth := make(chan Result) - go func() { - defer close(pushAuth) - status, success, err := c.pushAuth(ctx) - fmt.Println("-------------") - fmt.Println("pushAuth.status = ", status) - fmt.Println("pushAuth.success = ", success) - fmt.Println("pushAuth.err = ", err) - fmt.Println("-------------") - - select { - case <-ctx.Done(): // push cancelled by manual OTP - fmt.Println("pushAuth.func.timeoutCtx.Done()") - pushAuth <- Result{Error: nil, Status: "cancelled", Success: false} - default: - fmt.Println("pushAuth.func.default") - pushAuth <- Result{Error: err, Status: status, Success: success} - cancelTimeout() - } - }() - // Also allow manual OTP entry while waiting for push, with same timeout as push otpAuth := make(chan Result) go func() { defer close(otpAuth) - fmt.Println("twofactorverify: execute: waiting for user input") + ctxlog.Info("twofactorverify: execute: waiting for user input") otpAnswer := c.getOTP(ctx) - fmt.Println("otpAnswer = ", otpAnswer) select { - case <-ctx.Done(): // manual OTP cancelled by push - fmt.Println("otpAuth.func.timeoutCtx.Done()") - otpAuth <- Result{Error: nil, Status: "cancelled", Success: false} - default: - fmt.Println("otpAuth.func.timeoutCtx.default") - cancelTimeout() - fmt.Println("twofactorverify: execute: verifying entered OTP") - status, success, err := c.verifyOTP(ctx, otpAnswer) - ctxlog.WithError(err).Info("twofactorverify: execute: OTP verified") - otpAuth <- Result{Error: err, Status: status, Success: success} + case <-ctx.Done(): // manual OTP cancelled by push + fmt.Println("otpAuth.ctx.Done()") + otpAuth <- Result{Error: nil, Status: "cancelled", Success: false} + fmt.Println("----------------------------------------------------") + fmt.Println("otpAuth = ", otpAuth) + fmt.Println("----------------------------------------------------") + default: + fmt.Println("otpAuth.default") + cancelTimeout() + fmt.Println("Call c.verifyOTP(", ctx, ", ", otpAnswer, ")") + status, success, err := c.verifyOTP(ctx, otpAnswer) + fmt.Println("otpAnswer.status = ", status) + fmt.Println("otpAnswer.success = ", success) + fmt.Println("otpAnswer.err = ", err) + otpAuth <- Result{Error: err, Status: status, Success: success} + fmt.Println("----------------------------------------------------") + fmt.Println("otpAuth = ", otpAuth) + fmt.Println("----------------------------------------------------") } }() for { + //fmt.Println("for loop") select { - case pres := <- pushAuth: - fmt.Println("-------------") - fmt.Println("pushAuth = ", pres) - fmt.Println("-------------") - if len(pres.Status) > 0 { - fmt.Println("-------------") - fmt.Println("pushAuth = ", pres.Status) - fmt.Println("-------------") - } - case ores := <- otpAuth: - fmt.Println("-------------") - fmt.Println("otpAuth = ", ores) - fmt.Println("-------------") - if len(ores.Status) > 0 { - fmt.Println("-------------") - fmt.Println("otpAuth = ", ores.Status) - fmt.Println("-------------") - + case res := <- otpAuth: + fmt.Println(res) + //fmt.Println("-------------") + //fmt.Println("otpAuth = ", ores) + //fmt.Println("-------------") + if len(res.Status) > 0 && res.Status != "cancelled"{ + //fmt.Println("-------------") + //fmt.Println("otpAuth = ", res.Status) + //fmt.Println("-------------") + return res } } } @@ -283,11 +231,30 @@ func (c *Command) processCmd(ctx context.Context, cancelTimeout context.CancelFu +func (c *Command) getOTP(ctx context.Context) string { + prompt := "OTP: " + fmt.Fprint(c.ReadWriter.Out, prompt) + + var answer string + otpLength := int64(64) + reader := io.LimitReader(c.ReadWriter.In, otpLength) + if _, err := fmt.Fscanln(reader, &answer); err != nil { + 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 := "" - fmt.Println("verifyOTP(ctx, ",otp,")") + fmt.Println("verifyOTP(", ctx, ", ", c.Args, ", ",otp,")") success, reason, err = c.Client.VerifyOTP(ctx, c.Args, otp) + fmt.Println("----------------------------------------------------") + fmt.Println("verifyOTP.status = ", status) + fmt.Println("verifyOTP.success = ", success) + fmt.Println("verifyOTP.err = ", err) + fmt.Println("----------------------------------------------------") if success { status = fmt.Sprintf("\nOTP validation successful. Git operations are now allowed.\n") } else { @@ -304,14 +271,14 @@ func (c *Command) verifyOTP(ctx context.Context, otp string) (status string, suc } func (c *Command) pushAuth(ctx context.Context) (status string, success bool, err error) { - fmt.Println("---------------------------------------") + //fmt.Println("---------------------------------------") reason := "" - fmt.Println(c.Args) + //fmt.Println(c.Args) success, reason, err = c.Client.PushAuth(ctx, c.Args) - fmt.Println("pushAuth.reason = ", reason) - fmt.Println("pushAuth.success = ", success) - fmt.Println("pushAuth.err = ", err) - fmt.Println("---------------------------------------") + //fmt.Println("pushAuth.reason = ", reason) + //fmt.Println("pushAuth.success = ", success) + //fmt.Println("pushAuth.err = ", err) + //fmt.Println("---------------------------------------") if success { status = fmt.Sprintf("\nPush OTP validation successful. Git operations are now allowed.\n") } else { diff --git a/internal/command/twofactorverify/twofactorverifymanual_test.go b/internal/command/twofactorverify/twofactorverifymanual_test.go index 71372798..c914e778 100644 --- a/internal/command/twofactorverify/twofactorverifymanual_test.go +++ b/internal/command/twofactorverify/twofactorverifymanual_test.go @@ -110,24 +110,24 @@ func TestExecuteManual(t *testing.T) { 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", - //}, + { + 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 { diff --git a/internal/gitlabnet/twofactorverify/client.go b/internal/gitlabnet/twofactorverify/client.go index c9c13c8c..79498150 100644 --- a/internal/gitlabnet/twofactorverify/client.go +++ b/internal/gitlabnet/twofactorverify/client.go @@ -44,6 +44,11 @@ func (c *Client) VerifyOTP(ctx context.Context, args *commandargs.Shell, otp str } response, err := c.client.Post(ctx, "/two_factor_manual_otp_check", requestBody) + fmt.Println("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx") + fmt.Println("c.client = ", c.client) + fmt.Println("client.VerifyOTP.response = ", response) + fmt.Println("client.VerifyOTP.err = ", err) + fmt.Println("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx") if err != nil { return false, "", err } -- GitLab From 1fa30cb09115b72707d14b3b42d4c14a7ea6965b Mon Sep 17 00:00:00 2001 From: James Sandlin <jsandlin@gitlab.com> Date: Fri, 15 Jul 2022 15:00:17 -0700 Subject: [PATCH 24/25] waitgroup implemented & otp testing works --- .../twofactorverify/twofactorverify.go | 177 +++++++++++------- internal/gitlabnet/twofactorverify/client.go | 10 +- 2 files changed, 116 insertions(+), 71 deletions(-) diff --git a/internal/command/twofactorverify/twofactorverify.go b/internal/command/twofactorverify/twofactorverify.go index 23f3f8bd..88c49789 100644 --- a/internal/command/twofactorverify/twofactorverify.go +++ b/internal/command/twofactorverify/twofactorverify.go @@ -148,87 +148,132 @@ func (c *Command) Execute(ctx context.Context) error { return err } - // Create timeout context - myctx, mycancel := context.WithCancel(ctx) - defer mycancel() + waitGroup := sync.WaitGroup{} + + myctx, cancelCtx := context.WithTimeout(ctx, ctxMaxTime) + defer cancelCtx() + //myctx, mycancel := context.WithCancel(timeoutCtx) + + + + // Also allow manual OTP entry while waiting for push, with same timeout as push + otpChannel := make(chan Result) + waitGroup.Add(1) + //defer close(otpChannel) - otpAuth := make(chan Result) go func() { - defer close(otpAuth) + defer waitGroup.Done() ctxlog.Info("twofactorverify: execute: waiting for user input") otpAnswer := c.getOTP(myctx) select { case <-ctx.Done(): // manual OTP cancelled by push - otpAuth <- Result{Error: nil, Status: "cancelled", Success: false} + otpChannel <- Result{Error: nil, Status: "cancelled", Success: false} default: status, success, err := c.verifyOTP(myctx, otpAnswer) - otpAuth <- Result{Error: err, Status: status, Success: success} + otpChannel <- Result{Error: err, Status: status, Success: success} } + //cancelCtx() }() - for { - select { - case res := <- otpAuth: - if len(res.Status) > 0 && res.Status != "cancelled"{ - fmt.Fprint(c.ReadWriter.Out, res.Status) - return nil - } - } - } - - return nil -} - -func (c Command) processCmd(ctx context.Context, cancelTimeout context.CancelFunc) (result Result) { - ctxlog := log.ContextLogger(ctx) - - otpAuth := make(chan Result) + //// Background push notification with timeout + pushChannel := make(chan Result) + waitGroup.Add(1) go func() { - defer close(otpAuth) - ctxlog.Info("twofactorverify: execute: waiting for user input") - otpAnswer := c.getOTP(ctx) + defer waitGroup.Done() + //defer close(pushChannel) + ctxlog.Info("twofactorverify: execute: waiting for push auth") + //status, success, err := c.pushAuth(myctx) + //ctxlog.WithError(err).Info("twofactorverify: execute: push auth verified") select { - case <-ctx.Done(): // manual OTP cancelled by push - fmt.Println("otpAuth.ctx.Done()") - otpAuth <- Result{Error: nil, Status: "cancelled", Success: false} - fmt.Println("----------------------------------------------------") - fmt.Println("otpAuth = ", otpAuth) - fmt.Println("----------------------------------------------------") - default: - fmt.Println("otpAuth.default") - cancelTimeout() - fmt.Println("Call c.verifyOTP(", ctx, ", ", otpAnswer, ")") - status, success, err := c.verifyOTP(ctx, otpAnswer) - fmt.Println("otpAnswer.status = ", status) - fmt.Println("otpAnswer.success = ", success) - fmt.Println("otpAnswer.err = ", err) - otpAuth <- Result{Error: err, Status: status, Success: success} - fmt.Println("----------------------------------------------------") - fmt.Println("otpAuth = ", otpAuth) - fmt.Println("----------------------------------------------------") + case <-myctx.Done(): // push cancelled by manual OTP + // skip writing to channel + pushChannel <- Result{Error: nil, Status: "cancelled", Success: false} + ctxlog.Info("twofactorverify: execute: push auth cancelled") + //default: + // pushChannel <- Result{Error: err, Status: status, Success: success} } }() - for { - //fmt.Println("for loop") + //for { select { - case res := <- otpAuth: - fmt.Println(res) - //fmt.Println("-------------") - //fmt.Println("otpAuth = ", ores) - //fmt.Println("-------------") - if len(res.Status) > 0 && res.Status != "cancelled"{ - //fmt.Println("-------------") - //fmt.Println("otpAuth = ", res.Status) - //fmt.Println("-------------") - return res + case res := <-otpChannel: + //fmt.Println("Received from otpChannel => ", res) + if len(res.Status) > 0 && res.Status != "cancelled" { + fmt.Fprint(c.ReadWriter.Out, res.Status) + return nil } + case res := <-pushChannel: + if len(res.Status) > 0 && res.Status != "cancelled" { + //fmt.Println("Received from pushChannel => ", res) + fmt.Println("res.Status == ", res.Status, " -> ", len(res.Status)) + // //fmt.Println("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx") + // //fmt.Println(res) + // //fmt.Println("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx") + fmt.Fprint(c.ReadWriter.Out, res.Status) + return nil + } + //case <- myctx.Done(): + // fmt.Fprint(c.ReadWriter.Out, "\nOTP verification timed out\n") + // return nil + } - } - return -} + waitGroup.Wait() + //} + return nil +} +// +//func (c Command) processCmd(ctx context.Context, cancelTimeout context.CancelFunc) (result Result) { +// ctxlog := log.ContextLogger(ctx) +// +// otpAuth := make(chan Result) +// go func() { +// defer close(otpAuth) +// ctxlog.Info("twofactorverify: execute: waiting for user input") +// otpAnswer := c.getOTP(ctx) +// +// select { +// case <-ctx.Done(): // manual OTP cancelled by push +// fmt.Println("otpAuth.ctx.Done()") +// otpAuth <- Result{Error: nil, Status: "cancelled", Success: false} +// fmt.Println("----------------------------------------------------") +// fmt.Println("otpAuth = ", otpAuth) +// fmt.Println("----------------------------------------------------") +// default: +// fmt.Println("otpAuth.default") +// cancelTimeout() +// fmt.Println("Call c.verifyOTP(", ctx, ", ", otpAnswer, ")") +// status, success, err := c.verifyOTP(ctx, otpAnswer) +// fmt.Println("otpAnswer.status = ", status) +// fmt.Println("otpAnswer.success = ", success) +// fmt.Println("otpAnswer.err = ", err) +// otpAuth <- Result{Error: err, Status: status, Success: success} +// fmt.Println("----------------------------------------------------") +// fmt.Println("otpAuth = ", otpAuth) +// fmt.Println("----------------------------------------------------") +// } +// }() +// for { +// //fmt.Println("for loop") +// select { +// case res := <- otpAuth: +// fmt.Println(res) +// //fmt.Println("-------------") +// //fmt.Println("otpAuth = ", ores) +// //fmt.Println("-------------") +// if len(res.Status) > 0 && res.Status != "cancelled"{ +// //fmt.Println("-------------") +// //fmt.Println("otpAuth = ", res.Status) +// //fmt.Println("-------------") +// return res +// } +// } +// } +// return +//} +// +// func (c *Command) getOTP(ctx context.Context) string { @@ -248,13 +293,13 @@ func (c *Command) getOTP(ctx context.Context) string { func (c *Command) verifyOTP(ctx context.Context, otp string) (status string, success bool, err error) { reason := "" - fmt.Println("verifyOTP(", ctx, ", ", c.Args, ", ",otp,")") + //fmt.Println("verifyOTP(", ctx, ", ", c.Args, ", ",otp,")") success, reason, err = c.Client.VerifyOTP(ctx, c.Args, otp) - fmt.Println("----------------------------------------------------") - fmt.Println("verifyOTP.status = ", status) - fmt.Println("verifyOTP.success = ", success) - fmt.Println("verifyOTP.err = ", err) - fmt.Println("----------------------------------------------------") + //fmt.Println("----------------------------------------------------") + //fmt.Println("verifyOTP.status = ", status) + //fmt.Println("verifyOTP.success = ", success) + //fmt.Println("verifyOTP.err = ", err) + //fmt.Println("----------------------------------------------------") if success { status = fmt.Sprintf("\nOTP validation successful. Git operations are now allowed.\n") } else { diff --git a/internal/gitlabnet/twofactorverify/client.go b/internal/gitlabnet/twofactorverify/client.go index 79498150..fdd6d938 100644 --- a/internal/gitlabnet/twofactorverify/client.go +++ b/internal/gitlabnet/twofactorverify/client.go @@ -44,11 +44,11 @@ func (c *Client) VerifyOTP(ctx context.Context, args *commandargs.Shell, otp str } response, err := c.client.Post(ctx, "/two_factor_manual_otp_check", requestBody) - fmt.Println("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx") - fmt.Println("c.client = ", c.client) - fmt.Println("client.VerifyOTP.response = ", response) - fmt.Println("client.VerifyOTP.err = ", err) - fmt.Println("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx") + //fmt.Println("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx") + //fmt.Println("c.client = ", c.client) + //fmt.Println("client.VerifyOTP.response = ", response) + //fmt.Println("client.VerifyOTP.err = ", err) + //fmt.Println("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx") if err != nil { return false, "", err } -- GitLab From eb6033a66e054425fd43c8436fbc448907c0f160 Mon Sep 17 00:00:00 2001 From: James Sandlin <jsandlin@gitlab.com> Date: Fri, 15 Jul 2022 15:21:02 -0700 Subject: [PATCH 25/25] close; things are colliding --- .../twofactorverify/twofactorverify.go | 156 +++--------------- 1 file changed, 27 insertions(+), 129 deletions(-) diff --git a/internal/command/twofactorverify/twofactorverify.go b/internal/command/twofactorverify/twofactorverify.go index 88c49789..20e3df28 100644 --- a/internal/command/twofactorverify/twofactorverify.go +++ b/internal/command/twofactorverify/twofactorverify.go @@ -34,107 +34,6 @@ var ( ctxMaxTime = time.Second + 30 ) -func (c *Command) Execute2(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) - fmt.Println("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx") - fmt.Println("client = ", c.Client) - fmt.Println("err = ", err) - fmt.Println("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx") - 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) - verifyCtx, cancelVerify := context.WithCancel(timeoutCtx) - pushCtx, cancelPush := context.WithCancel(timeoutCtx) - defer cancelTimeout() - //fmt.Println("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx") - //fmt.Println(verifyCtx, ", ", cancelVerify) - //fmt.Println("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx") - // Background push notification with timeout - pushauth := make(chan Result) - go func() { - defer close(pushauth) - status, success, err := c.pushAuth(pushCtx) - - select { - case <-pushCtx.Done(): // push cancelled by manual OTP - pushauth <- Result{Error: nil, Status: "cancelled", Success: false} - default: - pushauth <- Result{Error: err, Status: status, Success: success} - cancelVerify() - } - }() - - // 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(verifyCtx) - - select { - case <-verifyCtx.Done(): // manual OTP cancelled by push - verify <- Result{Error: nil, Status: "cancelled", Success: false} - default: - cancelPush() - ctxlog.Info("twofactorverify: execute: verifying entered OTP") - status, success, err := c.verifyOTP(verifyCtx, answer) - //fmt.Println("-------------") - //fmt.Println("pushAuth.status = ", status) - //fmt.Println("pushAuth.success = ", success) - //fmt.Println("pushAuth.err = ", err) - //fmt.Println("-------------") - ctxlog.WithError(err).Info("twofactorverify: execute: OTP verified") - verify <- Result{Error: err, Status: status, Success: success} - } - }() - - - for { - select { - case res := <-verify: // manual OTP - //fmt.Println("-------------") - //fmt.Println("verify.res = ", res) - //fmt.Println("-------------") - if res.Status == "cancelled" { - // verify cancelled; don't print anything - } else if res.Status == "" { - // channel closed; don't print anything - } else { - fmt.Fprint(c.ReadWriter.Out, res.Status) - return nil - } - case res := <-pushauth: // push - //fmt.Println("-------------") - //fmt.Println("pushauth.res = ", res) - //fmt.Println("-------------") - if res.Status == "cancelled" { - // push cancelled; don't print anything - } else 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 - } - } - - return nil -} func (c *Command) Execute(ctx context.Context) error { ctxlog := log.ContextLogger(ctx) @@ -154,6 +53,8 @@ func (c *Command) Execute(ctx context.Context) error { defer cancelCtx() //myctx, mycancel := context.WithCancel(timeoutCtx) + myctx2, cancelCtx2 := context.WithTimeout(ctx, ctxMaxTime) + defer cancelCtx2() // Also allow manual OTP entry while waiting for push, with same timeout as push @@ -182,44 +83,41 @@ func (c *Command) Execute(ctx context.Context) error { defer waitGroup.Done() //defer close(pushChannel) ctxlog.Info("twofactorverify: execute: waiting for push auth") - //status, success, err := c.pushAuth(myctx) - //ctxlog.WithError(err).Info("twofactorverify: execute: push auth verified") + ctxlog.WithError(err).Info("twofactorverify: execute: push auth verified") select { - case <-myctx.Done(): // push cancelled by manual OTP + case <-myctx2.Done(): // push cancelled by manual OTP // skip writing to channel pushChannel <- Result{Error: nil, Status: "cancelled", Success: false} ctxlog.Info("twofactorverify: execute: push auth cancelled") - //default: - // pushChannel <- Result{Error: err, Status: status, Success: success} + default: + status, success, err := c.pushAuth(myctx2) + pushChannel <- Result{Error: err, Status: status, Success: success} } }() - //for { - select { - case res := <-otpChannel: - //fmt.Println("Received from otpChannel => ", res) - if len(res.Status) > 0 && res.Status != "cancelled" { - fmt.Fprint(c.ReadWriter.Out, res.Status) - return nil - } - case res := <-pushChannel: - if len(res.Status) > 0 && res.Status != "cancelled" { - //fmt.Println("Received from pushChannel => ", res) - fmt.Println("res.Status == ", res.Status, " -> ", len(res.Status)) - // //fmt.Println("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx") - // //fmt.Println(res) - // //fmt.Println("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx") - fmt.Fprint(c.ReadWriter.Out, res.Status) - return nil - } - //case <- myctx.Done(): - // fmt.Fprint(c.ReadWriter.Out, "\nOTP verification timed out\n") - // return nil - } + select { + case res := <-otpChannel: + //fmt.Println("Received from otpChannel => ", res) + if len(res.Status) > 0 && res.Status != "cancelled" { + fmt.Fprint(c.ReadWriter.Out, res.Status) + return nil + } + case res := <-pushChannel: + if len(res.Status) > 0 && res.Status != "cancelled" { + //fmt.Println("Received from pushChannel => ", res) + fmt.Println("res.Status == ", res.Status, " -> ", len(res.Status)) + fmt.Fprint(c.ReadWriter.Out, res.Status) + return nil + } + + case <- myctx.Done(): + fmt.Fprint(c.ReadWriter.Out, "\nOTP verification timed out\n") + return nil + + } waitGroup.Wait() - //} return nil } -- GitLab