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
@@ -4,6 +4,8 @@ import (
Loading
@@ -4,6 +4,8 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"sync"
"time"
"gitlab.com/gitlab-org/labkit/log" "gitlab.com/gitlab-org/labkit/log"
Loading
@@ -15,25 +17,162 @@ import (
Loading
@@ -15,25 +17,162 @@ import (
type Command struct { type Command struct {
Config *config.Config Config *config.Config
Client *twofactorverify.Client
Args *commandargs.Shell Args *commandargs.Shell
ReadWriter *readwriter.ReadWriter 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 { func (c *Command) Execute(ctx context.Context) error {
ctxlog := log.ContextLogger(ctx) ctxlog := log.ContextLogger(ctx)
ctxlog.Info("twofactorverify: execute: waiting for user input")
otp := c.getOTP(ctx)
ctxlog.Info("twofactorverify: execute: verifying entered OTP") // config.GetHTTPClient isn't thread-safe so save Client in struct for concurrency
err := c.verifyOTP(ctx, otp) // workaround until #518 is fixed
var err error
c.Client, err = twofactorverify.NewClient(c.Config)
if err != nil { if err != nil {
ctxlog.WithError(err).Error("twofactorverify: execute: OTP verification failed") ctxlog.WithError(err).Error("twofactorverify: execute: OTP verification failed")
return err 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 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 { func (c *Command) getOTP(ctx context.Context) string {
prompt := "OTP: " prompt := "OTP: "
Loading
@@ -49,18 +188,49 @@ func (c *Command) getOTP(ctx context.Context) string {
Loading
@@ -49,18 +188,49 @@ func (c *Command) getOTP(ctx context.Context) string {
return answer return answer
} }
func (c *Command) verifyOTP(ctx context.Context, otp string) error {
client, err := twofactorverify.NewClient(c.Config) func (c *Command) verifyOTP(ctx context.Context, otp string) (status string, success bool, err error) {
if err != nil { reason := ""
return err //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) err = nil
if err == nil {
fmt.Fprint(c.ReadWriter.Out, "\nOTP validation successful. Git operations are now allowed.\n") 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 { } 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
@@ -17,10 +17,10 @@ import (
Loading
@@ -17,10 +17,10 @@ import (
"gitlab.com/gitlab-org/gitlab-shell/internal/gitlabnet/twofactorverify" "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{ 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) { Handler: func(w http.ResponseWriter, r *http.Request) {
b, err := io.ReadAll(r.Body) b, err := io.ReadAll(r.Body)
defer r.Body.Close() defer r.Body.Close()
Loading
@@ -30,14 +30,15 @@ func setup(t *testing.T) []testserver.TestRequestHandler {
Loading
@@ -30,14 +30,15 @@ func setup(t *testing.T) []testserver.TestRequestHandler {
var requestBody *twofactorverify.RequestBody var requestBody *twofactorverify.RequestBody
require.NoError(t, json.Unmarshal(b, &requestBody)) require.NoError(t, json.Unmarshal(b, &requestBody))
var body map[string]interface{}
switch requestBody.KeyId { switch requestBody.KeyId {
case "1": case "1":
body := map[string]interface{}{ body = map[string]interface{}{
"success": true, "success": true,
} }
json.NewEncoder(w).Encode(body) json.NewEncoder(w).Encode(body)
case "error": case "error":
body := map[string]interface{}{ body = map[string]interface{}{
"success": false, "success": false,
"message": "error message", "message": "error message",
} }
Loading
@@ -53,12 +54,12 @@ func setup(t *testing.T) []testserver.TestRequestHandler {
Loading
@@ -53,12 +54,12 @@ func setup(t *testing.T) []testserver.TestRequestHandler {
} }
const ( const (
question = "OTP: \n" pushQuestion = "OTP: \n"
errorHeader = "OTP validation failed.\n" pushErrorHeader = "Push OTP validation failed.\n"
) )
func TestExecute(t *testing.T) { func TestExecutePush(t *testing.T) {
requests := setup(t) requests := setupPush(t)
url := testserver.StartSocketHttpServer(t, requests) url := testserver.StartSocketHttpServer(t, requests)
Loading
@@ -69,42 +70,32 @@ func TestExecute(t *testing.T) {
Loading
@@ -69,42 +70,32 @@ func TestExecute(t *testing.T) {
expectedOutput string expectedOutput string
}{ }{
{ {
desc: "With a known key id", desc: "When push is provided",
arguments: &commandargs.Shell{GitlabKeyId: "1"}, arguments: &commandargs.Shell{GitlabKeyId: "1"},
answer: "123456\n", answer: "",
expectedOutput: question + expectedOutput: pushQuestion + "Push OTP validation successful. Git operations are now allowed.\n",
"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: "With API returns an error", desc: "With API returns an error",
arguments: &commandargs.Shell{GitlabKeyId: "error"}, arguments: &commandargs.Shell{GitlabKeyId: "error"},
answer: "yes\n", answer: "",
expectedOutput: question + errorHeader + "error message\n", expectedOutput: pushQuestion + pushErrorHeader + "error message\n",
}, },
{ {
desc: "With API fails", desc: "With API fails",
arguments: &commandargs.Shell{GitlabKeyId: "broken"}, arguments: &commandargs.Shell{GitlabKeyId: "broken"},
answer: "yes\n", answer: "",
expectedOutput: question + errorHeader + "Internal API error (500)\n", expectedOutput: pushQuestion + pushErrorHeader + "Internal API error (500)\n",
},
{
desc: "With missing arguments",
arguments: &commandargs.Shell{},
answer: "yes\n",
expectedOutput: question + errorHeader + "who='' is invalid\n",
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) { t.Run(tc.desc, func(t *testing.T) {
output := &bytes.Buffer{} output := &bytes.Buffer{}
input := bytes.NewBufferString(tc.answer)
var input io.Reader
// make input wait for push auth tests
input, _ = io.Pipe()
cmd := &Command{ cmd := &Command{
Config: &config.Config{GitlabUrl: url}, Config: &config.Config{GitlabUrl: url},
Loading
Loading
Loading
@@ -2,7 +2,6 @@ package twofactorverify
Loading
@@ -2,7 +2,6 @@ package twofactorverify
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"net/http" "net/http"
Loading
@@ -38,32 +37,53 @@ func NewClient(config *config.Config) (*Client, error) {
Loading
@@ -38,32 +37,53 @@ func NewClient(config *config.Config) (*Client, error) {
return &Client{config: config, client: client}, nil 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) requestBody, err := c.getRequestBody(ctx, args, otp)
if err != nil { 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 { if err != nil {
return err return false, "", err
} }
defer response.Body.Close() defer response.Body.Close()
return parse(response) 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{} response := &Response{}
if err := gitlabnet.ParseJSON(hr, response); err != nil { if err := gitlabnet.ParseJSON(hr, response); err != nil {
return err return false, "", err
} }
if !response.Success { 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) (*RequestBody, error) {
Loading
Loading
Loading
@@ -16,10 +16,10 @@ import (
Loading
@@ -16,10 +16,10 @@ import (
"gitlab.com/gitlab-org/gitlab-shell/internal/config" "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{ 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) { Handler: func(w http.ResponseWriter, r *http.Request) {
b, err := io.ReadAll(r.Body) b, err := io.ReadAll(r.Body)
defer r.Body.Close() defer r.Body.Close()
Loading
@@ -78,35 +78,35 @@ func initialize(t *testing.T) []testserver.TestRequestHandler {
Loading
@@ -78,35 +78,35 @@ func initialize(t *testing.T) []testserver.TestRequestHandler {
} }
const ( const (
otpAttempt = "123456" manualOtpAttempt = "123456"
) )
func TestVerifyOTPByKeyId(t *testing.T) { func TestVerifyOTPByKeyId(t *testing.T) {
client := setup(t) client := setupManual(t)
args := &commandargs.Shell{GitlabKeyId: "0"} args := &commandargs.Shell{GitlabKeyId: "0"}
err := client.VerifyOTP(context.Background(), args, otpAttempt) _, _, err := client.VerifyOTP(context.Background(), args, manualOtpAttempt)
require.NoError(t, err) require.NoError(t, err)
} }
func TestVerifyOTPByUsername(t *testing.T) { func TestVerifyOTPByUsername(t *testing.T) {
client := setup(t) client := setupManual(t)
args := &commandargs.Shell{GitlabUsername: "jane-doe"} args := &commandargs.Shell{GitlabUsername: "jane-doe"}
err := client.VerifyOTP(context.Background(), args, otpAttempt) _, _, err := client.VerifyOTP(context.Background(), args, manualOtpAttempt)
require.NoError(t, err) require.NoError(t, err)
} }
func TestErrorMessage(t *testing.T) { func TestErrorMessage(t *testing.T) {
client := setup(t) client := setupManual(t)
args := &commandargs.Shell{GitlabKeyId: "1"} args := &commandargs.Shell{GitlabKeyId: "1"}
err := client.VerifyOTP(context.Background(), args, otpAttempt) _, reason, _ := client.VerifyOTP(context.Background(), args, manualOtpAttempt)
require.Equal(t, "error message", err.Error()) require.Equal(t, "error message", reason)
} }
func TestErrorResponses(t *testing.T) { func TestErrorResponses(t *testing.T) {
client := setup(t) client := setupManual(t)
testCases := []struct { testCases := []struct {
desc string desc string
Loading
@@ -133,15 +133,15 @@ func TestErrorResponses(t *testing.T) {
Loading
@@ -133,15 +133,15 @@ func TestErrorResponses(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) { t.Run(tc.desc, func(t *testing.T) {
args := &commandargs.Shell{GitlabKeyId: tc.fakeId} 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) require.EqualError(t, err, tc.expectedError)
}) })
} }
} }
func setup(t *testing.T) *Client { func setupManual(t *testing.T) *Client {
requests := initialize(t) requests := initializeManual(t)
url := testserver.StartSocketHttpServer(t, requests) url := testserver.StartSocketHttpServer(t, requests)
client, err := NewClient(&config.Config{GitlabUrl: url}) 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
@@ -3,7 +3,7 @@ require_relative 'spec_helper'
Loading
@@ -3,7 +3,7 @@ require_relative 'spec_helper'
require 'open3' require 'open3'
require 'json' require 'json'
describe 'bin/gitlab-shell 2fa_verify' do describe 'bin/gitlab-shell 2fa_verify manual' do
include_context 'gitlab shell' include_context 'gitlab shell'
let(:env) do let(:env) do
Loading
@@ -16,7 +16,7 @@ describe 'bin/gitlab-shell 2fa_verify' do
Loading
@@ -16,7 +16,7 @@ describe 'bin/gitlab-shell 2fa_verify' do
end end
def mock_server(server) 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.content_type = 'application/json'
res.status = 200 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