As we reevaluate how to best support and maintain Staging Ref in the future, we encourage development teams using this environment to highlight their use cases in the following issue: https://gitlab.com/gitlab-com/gl-infra/software-delivery/framework/software-delivery-framework-issue-tracker/-/issues/36.

Skip to content
Snippets Groups Projects
Commit f6feedf9 authored by Igor Drozdov's avatar Igor Drozdov
Browse files

Simplify 2FA Push auth processing

Use a single channel to handle both Push Auth and OTP results
parent fe5feeea
No related branches found
No related tags found
No related merge requests found
Loading
Loading
@@ -14,105 +14,63 @@ import (
"gitlab.com/gitlab-org/gitlab-shell/v14/internal/gitlabnet/twofactorverify"
)
const (
timeout = 30 * time.Second
prompt = "OTP: "
)
type Command struct {
Config *config.Config
Client *twofactorverify.Client
Args *commandargs.Shell
ReadWriter *readwriter.ReadWriter
}
type Result struct {
Error error
Status string
Success bool
}
func (c *Command) Execute(ctx context.Context) error {
ctxlog := log.ContextLogger(ctx)
// config.GetHTTPClient isn't thread-safe so save Client in struct for concurrency
// workaround until #518 is fixed
var err error
c.Client, err = twofactorverify.NewClient(c.Config)
client, err := twofactorverify.NewClient(c.Config)
if err != nil {
ctxlog.WithError(err).Error("twofactorverify: execute: OTP verification failed")
return err
}
// Create timeout context
// TODO: make timeout configurable
const ctxTimeout = 30
timeoutCtx, cancelTimeout := context.WithTimeout(ctx, ctxTimeout*time.Second)
defer cancelTimeout()
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
fmt.Fprint(c.ReadWriter.Out, prompt)
// Background push notification with timeout
pushauth := make(chan Result)
resultCh := make(chan string)
go func() {
defer close(pushauth)
ctxlog.Info("twofactorverify: execute: waiting for push auth")
status, success, err := c.pushAuth(timeoutCtx)
ctxlog.WithError(err).Info("twofactorverify: execute: push auth verified")
select {
case <-timeoutCtx.Done(): // push cancelled by manual OTP
// skip writing to channel
ctxlog.Info("twofactorverify: execute: push auth cancelled")
default:
pushauth <- Result{Error: err, Status: status, Success: success}
err := client.PushAuth(ctx, c.Args)
if err == nil {
resultCh <- "OTP has been validated by Push Authentication. Git operations are now allowed."
}
}()
// Also allow manual OTP entry while waiting for push, with same timeout as push
verify := make(chan Result)
go func() {
defer close(verify)
ctxlog.Info("twofactorverify: execute: waiting for user input")
answer := ""
answer = c.getOTP(timeoutCtx)
ctxlog.Info("twofactorverify: execute: user input received")
select {
case <-timeoutCtx.Done(): // manual OTP cancelled by push
// skip writing to channel
ctxlog.Info("twofactorverify: execute: verify cancelled")
default:
ctxlog.Info("twofactorverify: execute: verifying entered OTP")
status, success, err := c.verifyOTP(timeoutCtx, answer)
ctxlog.WithError(err).Info("twofactorverify: execute: OTP verified")
verify <- Result{Error: err, Status: status, Success: success}
answer, err := c.getOTP(ctx)
if err != nil {
resultCh <- formatErr(err)
}
}()
for {
select {
case res := <-verify:
if res.Status == "" {
// channel closed; don't print anything
} else {
fmt.Fprint(c.ReadWriter.Out, res.Status)
return nil
}
case res := <-pushauth:
if res.Status == "" {
// channel closed; don't print anything
} else {
fmt.Fprint(c.ReadWriter.Out, res.Status)
return nil
}
case <-timeoutCtx.Done(): // push timed out
fmt.Fprint(c.ReadWriter.Out, "\nOTP verification timed out\n")
return nil
if err := client.VerifyOTP(ctx, c.Args, answer); err != nil {
resultCh <- formatErr(err)
} else {
resultCh <- "OTP validation successful. Git operations are now allowed."
}
}()
var message string
select {
case message = <-resultCh:
case <-ctx.Done():
message = formatErr(ctx.Err())
}
log.WithContextFields(ctx, log.Fields{"message": message}).Info("Two factor verify command finished")
fmt.Fprintf(c.ReadWriter.Out, "\n%v\n", message)
return nil
}
func (c *Command) getOTP(ctx context.Context) string {
prompt := "OTP: "
fmt.Fprint(c.ReadWriter.Out, prompt)
func (c *Command) getOTP(ctx context.Context) (string, error) {
var answer string
otpLength := int64(64)
reader := io.LimitReader(c.ReadWriter.In, otpLength)
Loading
Loading
@@ -120,41 +78,13 @@ func (c *Command) getOTP(ctx context.Context) string {
log.ContextLogger(ctx).WithError(err).Debug("twofactorverify: getOTP: Failed to get user input")
}
return answer
}
func (c *Command) verifyOTP(ctx context.Context, otp string) (status string, success bool, err error) {
reason := ""
success, reason, err = c.Client.VerifyOTP(ctx, c.Args, otp)
if success {
status = fmt.Sprintf("\nOTP validation successful. Git operations are now allowed.\n")
} else {
if err != nil {
status = fmt.Sprintf("\nOTP validation failed.\n%v\n", err)
} else {
status = fmt.Sprintf("\nOTP validation failed.\n%v\n", reason)
}
if answer == "" {
return "", fmt.Errorf("OTP cannot be blank.")
}
err = nil
return
return answer, nil
}
func (c *Command) pushAuth(ctx context.Context) (status string, success bool, err error) {
reason := ""
success, reason, err = c.Client.PushAuth(ctx, c.Args)
if success {
status = fmt.Sprintf("\nPush OTP validation successful. Git operations are now allowed.\n")
} else {
if err != nil {
status = fmt.Sprintf("\nPush OTP validation failed.\n%v\n", err)
} else {
status = fmt.Sprintf("\nPush OTP validation failed.\n%v\n", reason)
}
}
return
func formatErr(err error) string {
return fmt.Sprintf("OTP validation failed: %v", err)
}
Loading
Loading
@@ -7,7 +7,6 @@ import (
"io"
"net/http"
"testing"
"time"
"github.com/stretchr/testify/require"
Loading
Loading
@@ -18,7 +17,17 @@ import (
"gitlab.com/gitlab-org/gitlab-shell/v14/internal/gitlabnet/twofactorverify"
)
func setupManual(t *testing.T) []testserver.TestRequestHandler {
type blockingReader struct{}
func (*blockingReader) Read([]byte) (int, error) {
waitInfinitely := make(chan struct{})
<-waitInfinitely
return 0, nil
}
func setup(t *testing.T) []testserver.TestRequestHandler {
waitInfinitely := make(chan struct{})
requests := []testserver.TestRequestHandler{
{
Path: "/api/v4/internal/two_factor_manual_otp_check",
Loading
Loading
@@ -31,15 +40,16 @@ func setupManual(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{}{
case "verify_via_otp", "verify_via_otp_with_push_error":
body := map[string]interface{}{
"success": true,
}
json.NewEncoder(w).Encode(body)
case "wait_infinitely":
<-waitInfinitely
case "error":
body = map[string]interface{}{
body := map[string]interface{}{
"success": false,
"message": "error message",
}
Loading
Loading
@@ -60,29 +70,16 @@ func setupManual(t *testing.T) []testserver.TestRequestHandler {
var requestBody *twofactorverify.RequestBody
require.NoError(t, json.Unmarshal(b, &requestBody))
time.Sleep(5 * time.Second)
var body map[string]interface{}
switch requestBody.KeyId {
case "1":
body = map[string]interface{}{
case "verify_via_push":
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":
case "verify_via_otp_with_push_error":
w.WriteHeader(http.StatusInternalServerError)
default:
body = map[string]interface{}{
"success": true,
"message": "default message",
}
json.NewEncoder(w).Encode(body)
<-waitInfinitely
}
},
},
Loading
Loading
@@ -91,45 +88,60 @@ func setupManual(t *testing.T) []testserver.TestRequestHandler {
return requests
}
const (
manualQuestion = "OTP: \n"
manualErrorHeader = "OTP validation failed.\n"
)
const errorHeader = "OTP validation failed: "
func TestExecuteManual(t *testing.T) {
requests := setupManual(t)
func TestExecute(t *testing.T) {
requests := setup(t)
url := testserver.StartSocketHttpServer(t, requests)
testCases := []struct {
desc string
arguments *commandargs.Shell
answer string
input io.Reader
expectedOutput string
}{
{
desc: "With a known key id",
arguments: &commandargs.Shell{GitlabKeyId: "1"},
answer: "123456\n",
expectedOutput: manualQuestion + "OTP validation successful. Git operations are now allowed.\n",
desc: "Verify via OTP",
arguments: &commandargs.Shell{GitlabKeyId: "verify_via_otp"},
expectedOutput: "OTP validation successful. Git operations are now allowed.\n",
},
{
desc: "Verify via OTP",
arguments: &commandargs.Shell{GitlabKeyId: "verify_via_otp_with_push_error"},
expectedOutput: "OTP validation successful. Git operations are now allowed.\n",
},
{
desc: "Verify via push authentication",
arguments: &commandargs.Shell{GitlabKeyId: "verify_via_push"},
input: &blockingReader{},
expectedOutput: "OTP has been validated by Push Authentication. Git operations are now allowed.\n",
},
{
desc: "With an empty OTP",
arguments: &commandargs.Shell{GitlabKeyId: "verify_via_otp"},
input: bytes.NewBufferString("\n"),
expectedOutput: errorHeader + "OTP cannot be blank.\n",
},
{
desc: "With bad response",
arguments: &commandargs.Shell{GitlabKeyId: "-1"},
answer: "123456\n",
expectedOutput: manualQuestion + manualErrorHeader + "Parsing failed\n",
expectedOutput: errorHeader + "Parsing failed\n",
},
{
desc: "With API returns an error",
arguments: &commandargs.Shell{GitlabKeyId: "error"},
answer: "yes\n",
expectedOutput: manualQuestion + manualErrorHeader + "error message\n",
expectedOutput: errorHeader + "error message\n",
},
{
desc: "With API fails",
arguments: &commandargs.Shell{GitlabKeyId: "broken"},
answer: "yes\n",
expectedOutput: manualQuestion + manualErrorHeader + "Internal API error (500)\n",
expectedOutput: errorHeader + "Internal API error (500)\n",
},
{
desc: "With missing arguments",
arguments: &commandargs.Shell{},
expectedOutput: errorHeader + "who='' is invalid\n",
},
}
Loading
Loading
@@ -137,7 +149,10 @@ func TestExecuteManual(t *testing.T) {
t.Run(tc.desc, func(t *testing.T) {
output := &bytes.Buffer{}
input := bytes.NewBufferString(tc.answer)
input := tc.input
if input == nil {
input = bytes.NewBufferString("123456\n")
}
cmd := &Command{
Config: &config.Config{GitlabUrl: url},
Loading
Loading
@@ -148,7 +163,29 @@ func TestExecuteManual(t *testing.T) {
err := cmd.Execute(context.Background())
require.NoError(t, err)
require.Equal(t, tc.expectedOutput, output.String())
require.Equal(t, prompt+"\n"+tc.expectedOutput, output.String())
})
}
}
func TestCanceledContext(t *testing.T) {
requests := setup(t)
output := &bytes.Buffer{}
url := testserver.StartSocketHttpServer(t, requests)
cmd := &Command{
Config: &config.Config{GitlabUrl: url},
Args: &commandargs.Shell{GitlabKeyId: "wait_infinitely"},
ReadWriter: &readwriter.ReadWriter{Out: output, In: &bytes.Buffer{}},
}
ctx, cancel := context.WithCancel(context.Background())
errCh := make(chan error)
go func() { errCh <- cmd.Execute(ctx) }()
cancel()
require.NoError(t, <-errCh)
require.Equal(t, prompt+"\n"+errorHeader+"context canceled\n", output.String())
}
package twofactorverify
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"testing"
"time"
"github.com/stretchr/testify/require"
"gitlab.com/gitlab-org/gitlab-shell/v14/client/testserver"
"gitlab.com/gitlab-org/gitlab-shell/v14/internal/command/commandargs"
"gitlab.com/gitlab-org/gitlab-shell/v14/internal/command/readwriter"
"gitlab.com/gitlab-org/gitlab-shell/v14/internal/config"
"gitlab.com/gitlab-org/gitlab-shell/v14/internal/gitlabnet/twofactorverify"
)
func setupPush(t *testing.T) []testserver.TestRequestHandler {
requests := []testserver.TestRequestHandler{
{
Path: "/api/v4/internal/two_factor_push_otp_check",
Handler: func(w http.ResponseWriter, r *http.Request) {
b, err := io.ReadAll(r.Body)
defer r.Body.Close()
require.NoError(t, err)
var requestBody *twofactorverify.RequestBody
require.NoError(t, json.Unmarshal(b, &requestBody))
var body map[string]interface{}
switch requestBody.KeyId {
case "1":
body = map[string]interface{}{
"success": true,
}
json.NewEncoder(w).Encode(body)
case "error":
body = map[string]interface{}{
"success": false,
"message": "error message",
}
require.NoError(t, json.NewEncoder(w).Encode(body))
case "broken":
w.WriteHeader(http.StatusInternalServerError)
}
},
},
{
Path: "/api/v4/internal/two_factor_manual_otp_check",
Handler: func(w http.ResponseWriter, r *http.Request) {
b, err := io.ReadAll(r.Body)
defer r.Body.Close()
require.NoError(t, err)
var requestBody *twofactorverify.RequestBody
require.NoError(t, json.Unmarshal(b, &requestBody))
time.Sleep(20 * time.Second)
var body map[string]interface{}
switch requestBody.KeyId {
case "1":
body = map[string]interface{}{
"success": true,
}
json.NewEncoder(w).Encode(body)
case "error":
body = map[string]interface{}{
"success": false,
"message": "error message",
}
require.NoError(t, json.NewEncoder(w).Encode(body))
case "broken":
w.WriteHeader(http.StatusInternalServerError)
}
},
},
}
return requests
}
const (
pushQuestion = "OTP: \n"
pushErrorHeader = "Push OTP validation failed.\n"
)
func TestExecutePush(t *testing.T) {
requests := setupPush(t)
url := testserver.StartSocketHttpServer(t, requests)
testCases := []struct {
desc string
arguments *commandargs.Shell
answer string
expectedOutput string
}{
{
desc: "When push is provided",
arguments: &commandargs.Shell{GitlabKeyId: "1"},
answer: "",
expectedOutput: pushQuestion + "Push OTP validation successful. Git operations are now allowed.\n",
},
{
desc: "With API returns an error",
arguments: &commandargs.Shell{GitlabKeyId: "error"},
answer: "",
expectedOutput: pushQuestion + pushErrorHeader + "error message\n",
},
{
desc: "With API fails",
arguments: &commandargs.Shell{GitlabKeyId: "broken"},
answer: "",
expectedOutput: pushQuestion + pushErrorHeader + "Internal API error (500)\n",
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
output := &bytes.Buffer{}
var input io.Reader
// make input wait for push auth tests
input, _ = io.Pipe()
cmd := &Command{
Config: &config.Config{GitlabUrl: url},
Args: tc.arguments,
ReadWriter: &readwriter.ReadWriter{Out: output, In: input},
}
err := cmd.Execute(context.Background())
require.NoError(t, err)
require.Equal(t, tc.expectedOutput, output.String())
})
}
}
Loading
Loading
@@ -2,6 +2,7 @@ package twofactorverify
import (
"context"
"errors"
"fmt"
"net/http"
Loading
Loading
@@ -25,7 +26,7 @@ type Response struct {
type RequestBody struct {
KeyId string `json:"key_id,omitempty"`
UserId int64 `json:"user_id,omitempty"`
OTPAttempt string `json:"otp_attempt"`
OTPAttempt string `json:"otp_attempt,omitempty"`
}
func NewClient(config *config.Config) (*Client, error) {
Loading
Loading
@@ -37,48 +38,47 @@ func NewClient(config *config.Config) (*Client, error) {
return &Client{config: config, client: client}, nil
}
func (c *Client) VerifyOTP(ctx context.Context, args *commandargs.Shell, otp string) (bool, string, error) {
func (c *Client) VerifyOTP(ctx context.Context, args *commandargs.Shell, otp string) error {
requestBody, err := c.getRequestBody(ctx, args, otp)
if err != nil {
return false, "", err
return err
}
response, err := c.client.Post(ctx, "/two_factor_manual_otp_check", requestBody)
if err != nil {
return false, "", err
return err
}
defer response.Body.Close()
return parse(response)
}
func (c *Client) PushAuth(ctx context.Context, args *commandargs.Shell) (bool, string, error) {
// enable push auth in internal rest api
func (c *Client) PushAuth(ctx context.Context, args *commandargs.Shell) error {
requestBody, err := c.getRequestBody(ctx, args, "")
if err != nil {
return false, "", err
return err
}
response, err := c.client.Post(ctx, "/two_factor_push_otp_check", requestBody)
if err != nil {
return false, "", err
return err
}
defer response.Body.Close()
return parse(response)
}
func parse(hr *http.Response) (bool, string, error) {
func parse(hr *http.Response) error {
response := &Response{}
if err := gitlabnet.ParseJSON(hr, response); err != nil {
return false, "", err
return err
}
if !response.Success {
return false, response.Message, nil
return errors.New(response.Message)
}
return true, response.Message, nil
return nil
}
func (c *Client) getRequestBody(ctx context.Context, args *commandargs.Shell, otp string) (*RequestBody, error) {
Loading
Loading
Loading
Loading
@@ -16,50 +16,56 @@ import (
"gitlab.com/gitlab-org/gitlab-shell/v14/internal/config"
)
func initializeManual(t *testing.T) []testserver.TestRequestHandler {
func initialize(t *testing.T) []testserver.TestRequestHandler {
handler := func(w http.ResponseWriter, r *http.Request) {
b, err := io.ReadAll(r.Body)
defer r.Body.Close()
require.NoError(t, err)
var requestBody *RequestBody
require.NoError(t, json.Unmarshal(b, &requestBody))
switch requestBody.KeyId {
case "0":
body := map[string]interface{}{
"success": true,
}
require.NoError(t, json.NewEncoder(w).Encode(body))
case "1":
body := map[string]interface{}{
"success": false,
"message": "error message",
}
require.NoError(t, json.NewEncoder(w).Encode(body))
case "2":
w.WriteHeader(http.StatusForbidden)
body := &client.ErrorResponse{
Message: "Not allowed!",
}
require.NoError(t, json.NewEncoder(w).Encode(body))
case "3":
w.Write([]byte("{ \"message\": \"broken json!\""))
case "4":
w.WriteHeader(http.StatusForbidden)
}
if requestBody.UserId == 1 {
body := map[string]interface{}{
"success": true,
}
require.NoError(t, json.NewEncoder(w).Encode(body))
}
}
requests := []testserver.TestRequestHandler{
{
Path: "/api/v4/internal/two_factor_manual_otp_check",
Handler: 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/two_factor_manual_otp_check",
Handler: handler,
},
{
Path: "/api/v4/internal/two_factor_push_otp_check",
Handler: handler,
},
{
Path: "/api/v4/internal/discover",
Loading
Loading
@@ -78,35 +84,86 @@ func initializeManual(t *testing.T) []testserver.TestRequestHandler {
}
const (
manualOtpAttempt = "123456"
otpAttempt = "123456"
)
func TestVerifyOTPByKeyId(t *testing.T) {
client := setupManual(t)
client := setup(t)
args := &commandargs.Shell{GitlabKeyId: "0"}
_, _, err := client.VerifyOTP(context.Background(), args, manualOtpAttempt)
err := client.VerifyOTP(context.Background(), args, otpAttempt)
require.NoError(t, err)
}
func TestVerifyOTPByUsername(t *testing.T) {
client := setupManual(t)
client := setup(t)
args := &commandargs.Shell{GitlabUsername: "jane-doe"}
_, _, err := client.VerifyOTP(context.Background(), args, manualOtpAttempt)
err := client.VerifyOTP(context.Background(), args, otpAttempt)
require.NoError(t, err)
}
func TestErrorMessage(t *testing.T) {
client := setupManual(t)
client := setup(t)
args := &commandargs.Shell{GitlabKeyId: "1"}
_, reason, _ := client.VerifyOTP(context.Background(), args, manualOtpAttempt)
require.Equal(t, "error message", reason)
err := client.VerifyOTP(context.Background(), args, otpAttempt)
require.Equal(t, "error message", err.Error())
}
func TestErrorResponses(t *testing.T) {
client := setupManual(t)
client := setup(t)
testCases := []struct {
desc string
fakeId string
expectedError string
}{
{
desc: "A response with an error message",
fakeId: "2",
expectedError: "Not allowed!",
},
{
desc: "A response with bad JSON",
fakeId: "3",
expectedError: "Parsing failed",
},
{
desc: "An error response without message",
fakeId: "4",
expectedError: "Internal API error (403)",
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
args := &commandargs.Shell{GitlabKeyId: tc.fakeId}
err := client.VerifyOTP(context.Background(), args, otpAttempt)
require.EqualError(t, err, tc.expectedError)
})
}
}
func TestVerifyPush(t *testing.T) {
client := setup(t)
args := &commandargs.Shell{GitlabKeyId: "0"}
err := client.PushAuth(context.Background(), args)
require.NoError(t, err)
}
func TestErrorMessagePush(t *testing.T) {
client := setup(t)
args := &commandargs.Shell{GitlabKeyId: "1"}
err := client.PushAuth(context.Background(), args)
require.Equal(t, "error message", err.Error())
}
func TestErrorResponsesPush(t *testing.T) {
client := setup(t)
testCases := []struct {
desc string
Loading
Loading
@@ -133,15 +190,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, manualOtpAttempt)
err := client.PushAuth(context.Background(), args)
require.EqualError(t, err, tc.expectedError)
})
}
}
func setupManual(t *testing.T) *Client {
requests := initializeManual(t)
func setup(t *testing.T) *Client {
requests := initialize(t)
url := testserver.StartSocketHttpServer(t, requests)
client, err := NewClient(&config.Config{GitlabUrl: url})
Loading
Loading
package twofactorverify
import (
"context"
"encoding/json"
"io"
"net/http"
"testing"
"gitlab.com/gitlab-org/gitlab-shell/v14/internal/gitlabnet/discover"
"github.com/stretchr/testify/require"
"gitlab.com/gitlab-org/gitlab-shell/v14/client"
"gitlab.com/gitlab-org/gitlab-shell/v14/client/testserver"
"gitlab.com/gitlab-org/gitlab-shell/v14/internal/command/commandargs"
"gitlab.com/gitlab-org/gitlab-shell/v14/internal/config"
)
func initializePush(t *testing.T) []testserver.TestRequestHandler {
requests := []testserver.TestRequestHandler{
{
Path: "/api/v4/internal/two_factor_push_otp_check",
Handler: func(w http.ResponseWriter, r *http.Request) {
b, err := io.ReadAll(r.Body)
defer r.Body.Close()
require.NoError(t, err)
var requestBody *RequestBody
require.NoError(t, json.Unmarshal(b, &requestBody))
switch requestBody.KeyId {
case "0":
body := map[string]interface{}{
"success": true,
}
require.NoError(t, json.NewEncoder(w).Encode(body))
case "1":
body := map[string]interface{}{
"success": false,
"message": "error message",
}
require.NoError(t, json.NewEncoder(w).Encode(body))
case "2":
w.WriteHeader(http.StatusForbidden)
body := &client.ErrorResponse{
Message: "Not allowed!",
}
require.NoError(t, json.NewEncoder(w).Encode(body))
case "3":
w.Write([]byte("{ \"message\": \"broken json!\""))
case "4":
w.WriteHeader(http.StatusForbidden)
}
if requestBody.UserId == 1 {
body := map[string]interface{}{
"success": true,
}
require.NoError(t, json.NewEncoder(w).Encode(body))
}
},
},
{
Path: "/api/v4/internal/discover",
Handler: func(w http.ResponseWriter, r *http.Request) {
body := &discover.Response{
UserId: 1,
Username: "jane-doe",
Name: "Jane Doe",
}
require.NoError(t, json.NewEncoder(w).Encode(body))
},
},
}
return requests
}
func TestVerifyPush(t *testing.T) {
client := setupPush(t)
args := &commandargs.Shell{GitlabKeyId: "0"}
_, _, err := client.PushAuth(context.Background(), args)
require.NoError(t, err)
}
func TestErrorMessagePush(t *testing.T) {
client := setupPush(t)
args := &commandargs.Shell{GitlabKeyId: "1"}
_, reason, _ := client.PushAuth(context.Background(), args)
require.Equal(t, "error message", reason)
}
func TestErrorResponsesPush(t *testing.T) {
client := setupPush(t)
testCases := []struct {
desc string
fakeId string
expectedError string
}{
{
desc: "A response with an error message",
fakeId: "2",
expectedError: "Not allowed!",
},
{
desc: "A response with bad JSON",
fakeId: "3",
expectedError: "Parsing failed",
},
{
desc: "An error response without message",
fakeId: "4",
expectedError: "Internal API error (403)",
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
args := &commandargs.Shell{GitlabKeyId: tc.fakeId}
_, _, err := client.PushAuth(context.Background(), args)
require.EqualError(t, err, tc.expectedError)
})
}
}
func setupPush(t *testing.T) *Client {
requests := initializePush(t)
url := testserver.StartSocketHttpServer(t, requests)
client, err := NewClient(&config.Config{GitlabUrl: url})
require.NoError(t, err)
return client
}
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
Loading
Loading
@@ -3,7 +3,7 @@ require_relative 'spec_helper'
require 'open3'
require 'json'
describe 'bin/gitlab-shell 2fa_verify manual' do
describe 'bin/gitlab-shell 2fa_verify' do
include_context 'gitlab shell'
let(:env) do
Loading
Loading
@@ -11,6 +11,8 @@ describe 'bin/gitlab-shell 2fa_verify manual' do
'SSH_ORIGINAL_COMMAND' => '2fa_verify' }
end
let(:correct_otp) { '123456' }
before(:context) do
write_config('gitlab_url' => "http+unix://#{CGI.escape(tmp_socket_path)}")
end
Loading
Loading
@@ -21,13 +23,12 @@ describe 'bin/gitlab-shell 2fa_verify manual' do
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: true }.to_json
else
res.body = { success: false, message: 'boom!' }.to_json
end
res.body = if params['otp_attempt'] == correct_otp
{ success: true }.to_json
else
{ success: false, message: 'boom!' }.to_json
end
end
server.mount_proc('/api/v4/internal/two_factor_push_otp_check') do |req, res|
Loading
Loading
@@ -35,58 +36,90 @@ describe 'bin/gitlab-shell 2fa_verify manual' do
res.status = 200
params = JSON.parse(req.body)
key_id = params['key_id'] || params['user_id'].to_s
id = params['key_id'] || params['user_id'].to_s
sleep 5 # simulate Fortinet API timing
res.body = { success: false, message: 'boom!' }.to_json
if id == '100'
res.body = { success: false, message: 'boom!' }.to_json
else
res.body = { success: true }.to_json
end
end
server.mount_proc('/api/v4/internal/discover') do |_, res|
server.mount_proc('/api/v4/internal/discover') do |req, res|
res.status = 200
res.content_type = 'application/json'
res.body = { id: 100, name: 'Some User', username: 'someuser' }.to_json
if req.query['username'] == 'someone'
res.body = { id: 100, name: 'Some User', username: 'someuser' }.to_json
else
res.body = { id: 101, name: 'Another User', username: 'another' }.to_json
end
end
end
describe 'command' do
context 'when key is provided' do
let(:cmd) { "#{gitlab_shell_path} key-100" }
describe 'entering OTP manually' do
let(:cmd) { "#{gitlab_shell_path} key-100" }
it 'prints a successful verification message' do
verify_successful_verification!(cmd)
context 'when key is provided' do
it 'asks a user for a correct OTP' do
verify_successful_otp_verification!(cmd)
end
end
context 'when username is provided' do
let(:cmd) { "#{gitlab_shell_path} username-someone" }
it 'prints a successful verification message' do
verify_successful_verification!(cmd)
it 'asks a user for a correct OTP' do
verify_successful_otp_verification!(cmd)
end
end
context 'when API error occurs' do
it 'shows an error when an invalid otp is provided' do
Open3.popen2(env, cmd) do |stdin, stdout|
asks_for_otp(stdout)
stdin.puts('000000')
expect(stdout.flush.read).to eq("\nOTP validation failed: boom!\n")
end
end
end
describe 'authorizing via push' do
context 'when key is provided' 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: ')
it 'asks a user for a correct OTP' do
verify_successful_push_verification!(cmd)
end
end
stdin.puts('123456')
context 'when username is provided' do
let(:cmd) { "#{gitlab_shell_path} username-another" }
expect(stdout.flush.read).to eq("\nOTP validation failed.\nboom!\n")
end
it 'asks a user for a correct OTP' do
verify_successful_push_verification!(cmd)
end
end
end
def verify_successful_verification!(cmd)
def verify_successful_otp_verification!(cmd)
Open3.popen2(env, cmd) do |stdin, stdout|
expect(stdout.gets(5)).to eq('OTP: ')
stdin.puts('123456')
asks_for_otp(stdout)
stdin.puts(correct_otp)
expect(stdout.flush.read).to eq("\nOTP validation successful. Git operations are now allowed.\n")
end
end
def verify_successful_push_verification!(cmd)
Open3.popen2(env, cmd) do |stdin, stdout|
asks_for_otp(stdout)
expect(stdout.flush.read).to eq("\nOTP has been validated by Push Authentication. Git operations are now allowed.\n")
end
end
def asks_for_otp(stdout)
expect(stdout.gets(5)).to eq('OTP: ')
end
end
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment