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

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • idrozdov/gitlab-shell
  • mmj/gitlab-shell
2 results
Show changes
Commits on Source (26)
Showing
with 1164 additions and 69 deletions
Loading
Loading
@@ -4,6 +4,8 @@ import (
"context"
"fmt"
"io"
"sync"
"time"
"gitlab.com/gitlab-org/labkit/log"
Loading
Loading
@@ -15,25 +17,162 @@ import (
type Command struct {
Config *config.Config
Client *twofactorverify.Client
Args *commandargs.Shell
ReadWriter *readwriter.ReadWriter
}
type Result struct {
Error error
Status string
Success bool
}
var (
mu sync.RWMutex
// TODO: make timeout configurable
ctxMaxTime = time.Second + 30
)
func (c *Command) Execute(ctx context.Context) error {
ctxlog := log.ContextLogger(ctx)
ctxlog.Info("twofactorverify: execute: waiting for user input")
otp := c.getOTP(ctx)
ctxlog.Info("twofactorverify: execute: verifying entered OTP")
err := c.verifyOTP(ctx, otp)
// 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
}
ctxlog.WithError(err).Info("twofactorverify: execute: OTP verified")
waitGroup := sync.WaitGroup{}
myctx, cancelCtx := context.WithTimeout(ctx, ctxMaxTime)
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
otpChannel := make(chan Result)
waitGroup.Add(1)
//defer close(otpChannel)
go func() {
defer waitGroup.Done()
ctxlog.Info("twofactorverify: execute: waiting for user input")
otpAnswer := c.getOTP(myctx)
select {
case <-ctx.Done(): // manual OTP cancelled by push
otpChannel <- Result{Error: nil, Status: "cancelled", Success: false}
default:
status, success, err := c.verifyOTP(myctx, otpAnswer)
otpChannel <- Result{Error: err, Status: status, Success: success}
}
//cancelCtx()
}()
//// Background push notification with timeout
pushChannel := make(chan Result)
waitGroup.Add(1)
go func() {
defer waitGroup.Done()
//defer close(pushChannel)
ctxlog.Info("twofactorverify: execute: waiting for push auth")
ctxlog.WithError(err).Info("twofactorverify: execute: push auth verified")
select {
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:
status, success, err := c.pushAuth(myctx2)
pushChannel <- Result{Error: err, Status: status, Success: success}
}
}()
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
}
//
//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 {
prompt := "OTP: "
Loading
Loading
@@ -49,18 +188,49 @@ func (c *Command) getOTP(ctx context.Context) string {
return answer
}
func (c *Command) verifyOTP(ctx context.Context, otp string) error {
client, err := twofactorverify.NewClient(c.Config)
if err != nil {
return err
func (c *Command) verifyOTP(ctx context.Context, otp string) (status string, success bool, err error) {
reason := ""
//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 {
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 = client.VerifyOTP(ctx, c.Args, otp)
if err == nil {
fmt.Fprint(c.ReadWriter.Out, "\nOTP validation successful. Git operations are now allowed.\n")
err = nil
return
}
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 {
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
}
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
}
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
}
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
}
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 setupManual(t *testing.T) []testserver.TestRequestHandler {
requests := []testserver.TestRequestHandler{
{
Path: "/api/v4/internal/two_factor_manual_otp_check",
Handler: func(w http.ResponseWriter, r *http.Request) {
b, err := io.ReadAll(r.Body)
defer r.Body.Close()
require.NoError(t, err)
var requestBody *twofactorverify.RequestBody
require.NoError(t, json.Unmarshal(b, &requestBody))
var body map[string]interface{}
switch requestBody.KeyId {
case "1":
body = map[string]interface{}{
"success": true,
}
json.NewEncoder(w).Encode(body)
case "error":
body = map[string]interface{}{
"success": false,
"message": "error message",
}
require.NoError(t, json.NewEncoder(w).Encode(body))
case "broken":
w.WriteHeader(http.StatusInternalServerError)
}
},
},
{
Path: "/api/v4/internal/two_factor_push_otp_check",
Handler: func(w http.ResponseWriter, r *http.Request) {
b, err := io.ReadAll(r.Body)
defer r.Body.Close()
require.NoError(t, err)
var requestBody *twofactorverify.RequestBody
require.NoError(t, json.Unmarshal(b, &requestBody))
var body map[string]interface{}
switch requestBody.KeyId {
case "1":
body = map[string]interface{}{
"success": true,
}
json.NewEncoder(w).Encode(body)
case "error":
body = map[string]interface{}{
"success": false,
"message": "error message",
}
require.NoError(t, json.NewEncoder(w).Encode(body))
case "broken":
w.WriteHeader(http.StatusInternalServerError)
default:
body = map[string]interface{}{
"success": true,
"message": "default message",
}
json.NewEncoder(w).Encode(body)
}
},
},
}
return requests
}
const (
manualQuestion = "OTP: \n"
manualErrorHeader = "OTP validation failed.\n"
)
func TestExecuteManual(t *testing.T) {
requests := setupManual(t)
url := testserver.StartSocketHttpServer(t, requests)
testCases := []struct {
desc string
arguments *commandargs.Shell
answer string
expectedOutput string
}{
{
desc: "With a known key id",
arguments: &commandargs.Shell{GitlabKeyId: "1"},
answer: "123456\n",
expectedOutput: manualQuestion + "OTP validation successful. Git operations are now allowed.\n",
},
{
desc: "With bad response",
arguments: &commandargs.Shell{GitlabKeyId: "-1"},
answer: "123456\n",
expectedOutput: manualQuestion + manualErrorHeader + "Parsing failed\n",
},
{
desc: "With API returns an error",
arguments: &commandargs.Shell{GitlabKeyId: "error"},
answer: "yes\n",
expectedOutput: manualQuestion + manualErrorHeader + "error message\n",
},
{
desc: "With API fails",
arguments: &commandargs.Shell{GitlabKeyId: "broken"},
answer: "yes\n",
expectedOutput: manualQuestion + manualErrorHeader + "Internal API error (500)\n",
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
output := &bytes.Buffer{}
input := bytes.NewBufferString(tc.answer)
cmd := &Command{
Config: &config.Config{GitlabUrl: url},
Args: tc.arguments,
ReadWriter: &readwriter.ReadWriter{Out: output, In: input},
}
err := cmd.Execute(context.Background())
require.NoError(t, err)
require.Equal(t, tc.expectedOutput, output.String())
})
}
}
Loading
Loading
@@ -17,10 +17,10 @@ import (
"gitlab.com/gitlab-org/gitlab-shell/internal/gitlabnet/twofactorverify"
)
func setup(t *testing.T) []testserver.TestRequestHandler {
func setupPush(t *testing.T) []testserver.TestRequestHandler {
requests := []testserver.TestRequestHandler{
{
Path: "/api/v4/internal/two_factor_otp_check",
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()
Loading
Loading
@@ -30,14 +30,15 @@ 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{}{
body = map[string]interface{}{
"success": true,
}
json.NewEncoder(w).Encode(body)
case "error":
body := map[string]interface{}{
body = map[string]interface{}{
"success": false,
"message": "error message",
}
Loading
Loading
@@ -53,12 +54,12 @@ func setup(t *testing.T) []testserver.TestRequestHandler {
}
const (
question = "OTP: \n"
errorHeader = "OTP validation failed.\n"
pushQuestion = "OTP: \n"
pushErrorHeader = "Push OTP validation failed.\n"
)
func TestExecute(t *testing.T) {
requests := setup(t)
func TestExecutePush(t *testing.T) {
requests := setupPush(t)
url := testserver.StartSocketHttpServer(t, requests)
Loading
Loading
@@ -69,42 +70,32 @@ func TestExecute(t *testing.T) {
expectedOutput string
}{
{
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",
arguments: &commandargs.Shell{GitlabKeyId: "-1"},
answer: "123456\n",
expectedOutput: question + errorHeader + "Parsing failed\n",
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: "yes\n",
expectedOutput: question + errorHeader + "error message\n",
answer: "",
expectedOutput: pushQuestion + pushErrorHeader + "error message\n",
},
{
desc: "With API fails",
arguments: &commandargs.Shell{GitlabKeyId: "broken"},
answer: "yes\n",
expectedOutput: question + errorHeader + "Internal API error (500)\n",
},
{
desc: "With missing arguments",
arguments: &commandargs.Shell{},
answer: "yes\n",
expectedOutput: question + errorHeader + "who='' is invalid\n",
answer: "",
expectedOutput: pushQuestion + pushErrorHeader + "Internal API error (500)\n",
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
output := &bytes.Buffer{}
input := bytes.NewBufferString(tc.answer)
var input io.Reader
// make input wait for push auth tests
input, _ = io.Pipe()
cmd := &Command{
Config: &config.Config{GitlabUrl: url},
Loading
Loading
Loading
Loading
@@ -2,7 +2,6 @@ package twofactorverify
import (
"context"
"errors"
"fmt"
"net/http"
Loading
Loading
@@ -38,32 +37,53 @@ 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 {
func (c *Client) VerifyOTP(ctx context.Context, args *commandargs.Shell, otp string) (bool, string, error) {
requestBody, err := c.getRequestBody(ctx, args, otp)
if err != nil {
return err
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)
//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 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, "")
if err != nil {
return false, "", err
}
response, err := c.client.Post(ctx, "/two_factor_push_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) {
Loading
Loading
Loading
Loading
@@ -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()
Loading
Loading
@@ -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"}
err := client.VerifyOTP(context.Background(), args, otpAttempt)
require.Equal(t, "error message", err.Error())
_, 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
Loading
Loading
@@ -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})
Loading
Loading
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
}
Loading
Loading
@@ -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
Loading
Loading
@@ -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
Loading
Loading
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