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
Unverified Commit 1293a330 authored by Imre Farkas's avatar Imre Farkas
Browse files

Add 2fa_verify command

parent 384f3036
No related branches found
No related tags found
No related merge requests found
Loading
Loading
@@ -15,6 +15,7 @@ import (
"gitlab.com/gitlab-org/gitlab-shell/internal/command/receivepack"
"gitlab.com/gitlab-org/gitlab-shell/internal/command/shared/disallowedcommand"
"gitlab.com/gitlab-org/gitlab-shell/internal/command/twofactorrecover"
"gitlab.com/gitlab-org/gitlab-shell/internal/command/twofactorverify"
"gitlab.com/gitlab-org/gitlab-shell/internal/command/uploadarchive"
"gitlab.com/gitlab-org/gitlab-shell/internal/command/uploadpack"
"gitlab.com/gitlab-org/gitlab-shell/internal/config"
Loading
Loading
@@ -87,6 +88,8 @@ func buildShellCommand(args *commandargs.Shell, config *config.Config, readWrite
return &discover.Command{Config: config, Args: args, ReadWriter: readWriter}
case commandargs.TwoFactorRecover:
return &twofactorrecover.Command{Config: config, Args: args, ReadWriter: readWriter}
case commandargs.TwoFactorVerify:
return &twofactorverify.Command{Config: config, Args: args, ReadWriter: readWriter}
case commandargs.LfsAuthenticate:
return &lfsauthenticate.Command{Config: config, Args: args, ReadWriter: readWriter}
case commandargs.ReceivePack:
Loading
Loading
Loading
Loading
@@ -16,6 +16,7 @@ import (
"gitlab.com/gitlab-org/gitlab-shell/internal/command/receivepack"
"gitlab.com/gitlab-org/gitlab-shell/internal/command/shared/disallowedcommand"
"gitlab.com/gitlab-org/gitlab-shell/internal/command/twofactorrecover"
"gitlab.com/gitlab-org/gitlab-shell/internal/command/twofactorverify"
"gitlab.com/gitlab-org/gitlab-shell/internal/command/uploadarchive"
"gitlab.com/gitlab-org/gitlab-shell/internal/command/uploadpack"
"gitlab.com/gitlab-org/gitlab-shell/internal/config"
Loading
Loading
@@ -75,6 +76,14 @@ func TestNew(t *testing.T) {
expectedType: &twofactorrecover.Command{},
expectedSslCertDir: "",
},
{
desc: "it returns a TwoFactorVerify command",
executable: gitlabShellExec,
environment: buildEnv("2fa_verify"),
config: basicConfig,
expectedType: &twofactorverify.Command{},
expectedSslCertDir: "",
},
{
desc: "it returns an LfsAuthenticate command",
executable: gitlabShellExec,
Loading
Loading
Loading
Loading
@@ -11,6 +11,7 @@ import (
const (
Discover CommandType = "discover"
TwoFactorRecover CommandType = "2fa_recovery_codes"
TwoFactorVerify CommandType = "2fa_verify"
LfsAuthenticate CommandType = "git-lfs-authenticate"
ReceivePack CommandType = "git-receive-pack"
UploadPack CommandType = "git-upload-pack"
Loading
Loading
package twofactorverify
import (
"context"
"fmt"
"io"
"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
Args *commandargs.Shell
ReadWriter *readwriter.ReadWriter
}
func (c *Command) Execute(ctx context.Context) error {
err := c.verifyOTP(ctx, c.getOTP())
if err != nil {
return err
}
return nil
}
func (c *Command) getOTP() string {
prompt := "OTP: "
fmt.Fprint(c.ReadWriter.Out, prompt)
var answer string
otpLength := int64(64)
reader := io.LimitReader(c.ReadWriter.In, otpLength)
fmt.Fscanln(reader, &answer)
return answer
}
func (c *Command) verifyOTP(ctx context.Context, otp string) error {
client, err := twofactorverify.NewClient(c.Config)
if err != nil {
return err
}
err = client.VerifyOTP(ctx, c.Args, otp)
if err == nil {
fmt.Fprint(c.ReadWriter.Out, "\nOTP validation successful. Git operations are allowed for the next 15 minutes.\n")
} else {
fmt.Fprintf(c.ReadWriter.Out, "\nOTP validation failed.\n%v\n", err)
}
return nil
}
package twofactorverify
import (
"bytes"
"context"
"encoding/json"
"io/ioutil"
"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 setup(t *testing.T) []testserver.TestRequestHandler {
requests := []testserver.TestRequestHandler{
{
Path: "/api/v4/internal/two_factor_otp_check",
Handler: func(w http.ResponseWriter, r *http.Request) {
b, err := ioutil.ReadAll(r.Body)
defer r.Body.Close()
require.NoError(t, err)
var requestBody *twofactorverify.RequestBody
require.NoError(t, json.Unmarshal(b, &requestBody))
switch requestBody.KeyId {
case "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 (
question = "OTP: \n"
errorHeader = "OTP validation failed.\n"
)
func TestExecute(t *testing.T) {
requests := setup(t)
url, cleanup := testserver.StartSocketHttpServer(t, requests)
defer cleanup()
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: question +
"OTP validation successful. Git operations are allowed for the next 15 minutes.\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",
arguments: &commandargs.Shell{GitlabKeyId: "error"},
answer: "yes\n",
expectedOutput: question + errorHeader + "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",
},
}
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())
})
}
}
package twofactorverify
import (
"context"
"errors"
"fmt"
"net/http"
"gitlab.com/gitlab-org/gitlab-shell/client"
"gitlab.com/gitlab-org/gitlab-shell/internal/command/commandargs"
"gitlab.com/gitlab-org/gitlab-shell/internal/config"
"gitlab.com/gitlab-org/gitlab-shell/internal/gitlabnet"
"gitlab.com/gitlab-org/gitlab-shell/internal/gitlabnet/discover"
)
type Client struct {
config *config.Config
client *client.GitlabNetClient
}
type Response struct {
Success bool `json:"success"`
Message string `json:"message"`
}
type RequestBody struct {
KeyId string `json:"key_id,omitempty"`
UserId int64 `json:"user_id,omitempty"`
OTPAttempt string `json:"otp_attempt"`
}
func NewClient(config *config.Config) (*Client, error) {
client, err := gitlabnet.GetClient(config)
if err != nil {
return nil, fmt.Errorf("Error creating http client: %v", err)
}
return &Client{config: config, client: client}, nil
}
func (c *Client) VerifyOTP(ctx context.Context, args *commandargs.Shell, otp string) error {
requestBody, err := c.getRequestBody(ctx, args, otp)
if err != nil {
return err
}
response, err := c.client.Post(ctx, "/two_factor_otp_check", requestBody)
if err != nil {
return err
}
defer response.Body.Close()
return parse(response)
}
func parse(hr *http.Response) error {
response := &Response{}
if err := gitlabnet.ParseJSON(hr, response); err != nil {
return err
}
if !response.Success {
return errors.New(response.Message)
}
return nil
}
func (c *Client) getRequestBody(ctx context.Context, args *commandargs.Shell, otp string) (*RequestBody, error) {
client, err := discover.NewClient(c.config)
if err != nil {
return nil, err
}
var requestBody *RequestBody
if args.GitlabKeyId != "" {
requestBody = &RequestBody{KeyId: args.GitlabKeyId, OTPAttempt: otp}
} else {
userInfo, err := client.GetByCommandArgs(ctx, args)
if err != nil {
return nil, err
}
requestBody = &RequestBody{UserId: userInfo.UserId, OTPAttempt: otp}
}
return requestBody, nil
}
package twofactorverify
import (
"context"
"encoding/json"
"gitlab.com/gitlab-org/gitlab-shell/internal/gitlabnet/discover"
"io/ioutil"
"net/http"
"testing"
"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 initialize(t *testing.T) []testserver.TestRequestHandler {
requests := []testserver.TestRequestHandler{
{
Path: "/api/v4/internal/two_factor_otp_check",
Handler: func(w http.ResponseWriter, r *http.Request) {
b, err := ioutil.ReadAll(r.Body)
defer r.Body.Close()
require.NoError(t, err)
var requestBody *RequestBody
require.NoError(t, json.Unmarshal(b, &requestBody))
switch requestBody.KeyId {
case "0":
body := map[string]interface{}{
"success": true,
}
require.NoError(t, json.NewEncoder(w).Encode(body))
case "1":
body := map[string]interface{}{
"success": false,
"message": "error message",
}
require.NoError(t, json.NewEncoder(w).Encode(body))
case "2":
w.WriteHeader(http.StatusForbidden)
body := &client.ErrorResponse{
Message: "Not allowed!",
}
require.NoError(t, json.NewEncoder(w).Encode(body))
case "3":
w.Write([]byte("{ \"message\": \"broken json!\""))
case "4":
w.WriteHeader(http.StatusForbidden)
}
if requestBody.UserId == 1 {
body := map[string]interface{}{
"success": true,
}
require.NoError(t, json.NewEncoder(w).Encode(body))
}
},
},
{
Path: "/api/v4/internal/discover",
Handler: func(w http.ResponseWriter, r *http.Request) {
body := &discover.Response{
UserId: 1,
Username: "jane-doe",
Name: "Jane Doe",
}
require.NoError(t, json.NewEncoder(w).Encode(body))
},
},
}
return requests
}
const (
otpAttempt = "123456"
)
func TestVerifyOTPByKeyId(t *testing.T) {
client, cleanup := setup(t)
defer cleanup()
args := &commandargs.Shell{GitlabKeyId: "0"}
err := client.VerifyOTP(context.Background(), args, otpAttempt)
require.NoError(t, err)
}
func TestVerifyOTPByUsername(t *testing.T) {
client, cleanup := setup(t)
defer cleanup()
args := &commandargs.Shell{GitlabUsername: "jane-doe"}
err := client.VerifyOTP(context.Background(), args, otpAttempt)
require.NoError(t, err)
}
func TestErrorMessage(t *testing.T) {
client, cleanup := setup(t)
defer cleanup()
args := &commandargs.Shell{GitlabKeyId: "1"}
err := client.VerifyOTP(context.Background(), args, otpAttempt)
require.Equal(t, "error message", err.Error())
}
func TestErrorResponses(t *testing.T) {
client, cleanup := setup(t)
defer cleanup()
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 setup(t *testing.T) (*Client, func()) {
requests := initialize(t)
url, cleanup := testserver.StartSocketHttpServer(t, requests)
client, err := NewClient(&config.Config{GitlabUrl: url})
require.NoError(t, err)
return client, cleanup
}
require_relative 'spec_helper'
require 'open3'
require 'json'
describe 'bin/gitlab-shell 2fa_verify' 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_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: 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 key is provided' do
let(:cmd) { "#{gitlab_shell_path} key-100" }
it 'prints a successful verification message' do
verify_successful_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)
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: ')
stdin.puts('123456')
expect(stdout.flush.read).to eq("\nOTP validation failed.\nboom!\n")
end
end
end
end
def verify_successful_verification!(cmd)
Open3.popen2(env, cmd) do |stdin, stdout|
expect(stdout.gets(5)).to eq('OTP: ')
stdin.puts('123456')
expect(stdout.flush.read).to eq("\nOTP validation successful. Git operations are allowed for the next 15 minutes.\n")
end
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