Commit 3b0176df authored by Patrick Bajao's avatar Patrick Bajao

Support different CommandArgs type

`CommandArgs` has been renamed to `Shell`.

An interface has been added that includes `Executable()` and
`Arguments()` method. The `BaseArgs` implement this methods
and should be embeeded in each type.
parent aab85f36
...@@ -23,16 +23,27 @@ func New(arguments []string, config *config.Config, readWriter *readwriter.ReadW ...@@ -23,16 +23,27 @@ func New(arguments []string, config *config.Config, readWriter *readwriter.ReadW
return nil, err return nil, err
} }
if config.FeatureEnabled(string(args.CommandType)) { if cmd := buildCommand(args, config, readWriter); cmd != nil {
if cmd := buildCommand(args, config, readWriter); cmd != nil { return cmd, nil
return cmd, nil
}
} }
return &fallback.Command{RootDir: config.RootDir, Args: args}, nil return &fallback.Command{RootDir: config.RootDir, Args: args}, nil
} }
func buildCommand(args *commandargs.CommandArgs, config *config.Config, readWriter *readwriter.ReadWriter) Command { func buildCommand(args commandargs.CommandArgs, config *config.Config, readWriter *readwriter.ReadWriter) Command {
switch args.Executable() {
case commandargs.GitlabShell:
return buildShellCommand(args.(*commandargs.Shell), config, readWriter)
}
return nil
}
func buildShellCommand(args *commandargs.Shell, config *config.Config, readWriter *readwriter.ReadWriter) Command {
if !config.FeatureEnabled(string(args.CommandType)) {
return nil
}
switch args.CommandType { switch args.CommandType {
case commandargs.Discover: case commandargs.Discover:
return &discover.Command{Config: config, Args: args, ReadWriter: readWriter} return &discover.Command{Config: config, Args: args, ReadWriter: readWriter}
......
...@@ -129,6 +129,12 @@ func TestNew(t *testing.T) { ...@@ -129,6 +129,12 @@ func TestNew(t *testing.T) {
arguments: []string{string(commandargs.GitlabShell)}, arguments: []string{string(commandargs.GitlabShell)},
expectedType: &fallback.Command{}, expectedType: &fallback.Command{},
}, },
{
desc: "it returns a Fallback command if executable is unknown",
config: &config.Config{},
arguments: []string{"unknown"},
expectedType: &fallback.Command{},
},
} }
for _, tc := range testCases { for _, tc := range testCases {
...@@ -145,12 +151,9 @@ func TestNew(t *testing.T) { ...@@ -145,12 +151,9 @@ func TestNew(t *testing.T) {
} }
func TestFailingNew(t *testing.T) { func TestFailingNew(t *testing.T) {
t.Run("It returns an error when SSH_CONNECTION is not set", func(t *testing.T) { t.Run("It returns an error parsing arguments failed", func(t *testing.T) {
restoreEnv := testhelper.TempEnv(map[string]string{}) _, err := New([]string{}, &config.Config{}, nil)
defer restoreEnv()
_, err := New([]string{string(commandargs.GitlabShell)}, &config.Config{}, nil)
require.Error(t, err, "Only ssh allowed") require.Error(t, err)
}) })
} }
package commandargs
import (
"errors"
"path/filepath"
)
type BaseArgs struct {
arguments []string
}
func (b *BaseArgs) Parse() error {
if b.hasEmptyArguments() {
return errors.New("arguments should include the executable")
}
return nil
}
func (b *BaseArgs) Executable() Executable {
if b.hasEmptyArguments() {
return Executable("")
}
return Executable(filepath.Base(b.arguments[0]))
}
func (b *BaseArgs) Arguments() []string {
return b.arguments[1:]
}
func (b *BaseArgs) hasEmptyArguments() bool {
return len(b.arguments) == 0
}
package commandargs package commandargs
import (
"errors"
"os"
"path/filepath"
"regexp"
"github.com/mattn/go-shellwords"
)
type CommandType string type CommandType string
type Executable string type Executable string
const ( const (
Discover CommandType = "discover" GitlabShell Executable = "gitlab-shell"
TwoFactorRecover CommandType = "2fa_recovery_codes"
LfsAuthenticate CommandType = "git-lfs-authenticate"
ReceivePack CommandType = "git-receive-pack"
UploadPack CommandType = "git-upload-pack"
UploadArchive CommandType = "git-upload-archive"
GitlabShell Executable = "gitlab-shell"
)
var (
whoKeyRegex = regexp.MustCompile(`\bkey-(?P<keyid>\d+)\b`)
whoUsernameRegex = regexp.MustCompile(`\busername-(?P<username>\S+)\b`)
) )
type CommandArgs struct { type CommandArgs interface {
arguments []string Parse() error
GitlabUsername string Executable() Executable
GitlabKeyId string Arguments() []string
SshArgs []string
CommandType CommandType
}
func Parse(arguments []string) (*CommandArgs, error) {
args := &CommandArgs{arguments: arguments}
if args.Executable() == GitlabShell {
if sshConnection := os.Getenv("SSH_CONNECTION"); sshConnection == "" {
return nil, errors.New("Only ssh allowed")
}
args.parseWho()
if err := args.parseCommand(os.Getenv("SSH_ORIGINAL_COMMAND")); err != nil {
return nil, errors.New("Invalid ssh command")
}
args.defineCommandType()
return args, nil
} else {
return args, nil
}
}
func (c *CommandArgs) Executable() Executable {
return Executable(filepath.Base(c.arguments[0]))
}
func (c *CommandArgs) Arguments() []string {
return c.arguments[1:]
} }
func (c *CommandArgs) parseWho() { func Parse(arguments []string) (CommandArgs, error) {
for _, argument := range c.arguments { var args CommandArgs = &BaseArgs{arguments: arguments}
if keyId := tryParseKeyId(argument); keyId != "" {
c.GitlabKeyId = keyId
break
}
if username := tryParseUsername(argument); username != "" { switch args.Executable() {
c.GitlabUsername = username case GitlabShell:
break args = &Shell{BaseArgs: args.(*BaseArgs)}
}
} }
}
func tryParseKeyId(argument string) string {
matchInfo := whoKeyRegex.FindStringSubmatch(argument)
if len(matchInfo) == 2 {
// The first element is the full matched string
// The second element is the named `keyid`
return matchInfo[1]
}
return ""
}
func tryParseUsername(argument string) string {
matchInfo := whoUsernameRegex.FindStringSubmatch(argument)
if len(matchInfo) == 2 {
// The first element is the full matched string
// The second element is the named `username`
return matchInfo[1]
}
return ""
}
func (c *CommandArgs) parseCommand(commandString string) error {
args, err := shellwords.Parse(commandString)
if err != nil {
return err
}
// Handle Git for Windows 2.14 using "git upload-pack" instead of git-upload-pack
if len(args) > 1 && args[0] == "git" {
command := args[0] + "-" + args[1]
commandArgs := args[2:]
args = append([]string{command}, commandArgs...) if err := args.Parse(); err != nil {
return nil, err
} }
c.SshArgs = args return args, nil
return nil
}
func (c *CommandArgs) defineCommandType() {
if len(c.SshArgs) == 0 {
c.CommandType = Discover
} else {
c.CommandType = CommandType(c.SshArgs[0])
}
} }
...@@ -13,7 +13,7 @@ func TestParseSuccess(t *testing.T) { ...@@ -13,7 +13,7 @@ func TestParseSuccess(t *testing.T) {
desc string desc string
environment map[string]string environment map[string]string
arguments []string arguments []string
expectedArgs *CommandArgs expectedArgs CommandArgs
}{ }{
// Setting the used env variables for every case to ensure we're // Setting the used env variables for every case to ensure we're
// not using anything set in the original env. // not using anything set in the original env.
...@@ -24,7 +24,7 @@ func TestParseSuccess(t *testing.T) { ...@@ -24,7 +24,7 @@ func TestParseSuccess(t *testing.T) {
"SSH_ORIGINAL_COMMAND": "", "SSH_ORIGINAL_COMMAND": "",
}, },
arguments: []string{string(GitlabShell)}, arguments: []string{string(GitlabShell)},
expectedArgs: &CommandArgs{arguments: []string{string(GitlabShell)}, SshArgs: []string{}, CommandType: Discover}, expectedArgs: &Shell{BaseArgs: &BaseArgs{arguments: []string{string(GitlabShell)}}, SshArgs: []string{}, CommandType: Discover},
}, },
{ {
desc: "It finds the key id in any passed arguments", desc: "It finds the key id in any passed arguments",
...@@ -33,7 +33,7 @@ func TestParseSuccess(t *testing.T) { ...@@ -33,7 +33,7 @@ func TestParseSuccess(t *testing.T) {
"SSH_ORIGINAL_COMMAND": "", "SSH_ORIGINAL_COMMAND": "",
}, },
arguments: []string{string(GitlabShell), "hello", "key-123"}, arguments: []string{string(GitlabShell), "hello", "key-123"},
expectedArgs: &CommandArgs{arguments: []string{string(GitlabShell), "hello", "key-123"}, SshArgs: []string{}, CommandType: Discover, GitlabKeyId: "123"}, expectedArgs: &Shell{BaseArgs: &BaseArgs{arguments: []string{string(GitlabShell), "hello", "key-123"}}, SshArgs: []string{}, CommandType: Discover, GitlabKeyId: "123"},
}, { }, {
desc: "It finds the username in any passed arguments", desc: "It finds the username in any passed arguments",
environment: map[string]string{ environment: map[string]string{
...@@ -41,7 +41,7 @@ func TestParseSuccess(t *testing.T) { ...@@ -41,7 +41,7 @@ func TestParseSuccess(t *testing.T) {
"SSH_ORIGINAL_COMMAND": "", "SSH_ORIGINAL_COMMAND": "",
}, },
arguments: []string{string(GitlabShell), "hello", "username-jane-doe"}, arguments: []string{string(GitlabShell), "hello", "username-jane-doe"},
expectedArgs: &CommandArgs{arguments: []string{string(GitlabShell), "hello", "username-jane-doe"}, SshArgs: []string{}, CommandType: Discover, GitlabUsername: "jane-doe"}, expectedArgs: &Shell{BaseArgs: &BaseArgs{arguments: []string{string(GitlabShell), "hello", "username-jane-doe"}}, SshArgs: []string{}, CommandType: Discover, GitlabUsername: "jane-doe"},
}, { }, {
desc: "It parses 2fa_recovery_codes command", desc: "It parses 2fa_recovery_codes command",
environment: map[string]string{ environment: map[string]string{
...@@ -49,7 +49,7 @@ func TestParseSuccess(t *testing.T) { ...@@ -49,7 +49,7 @@ func TestParseSuccess(t *testing.T) {
"SSH_ORIGINAL_COMMAND": "2fa_recovery_codes", "SSH_ORIGINAL_COMMAND": "2fa_recovery_codes",
}, },
arguments: []string{string(GitlabShell)}, arguments: []string{string(GitlabShell)},
expectedArgs: &CommandArgs{arguments: []string{string(GitlabShell)}, SshArgs: []string{"2fa_recovery_codes"}, CommandType: TwoFactorRecover}, expectedArgs: &Shell{BaseArgs: &BaseArgs{arguments: []string{string(GitlabShell)}}, SshArgs: []string{"2fa_recovery_codes"}, CommandType: TwoFactorRecover},
}, { }, {
desc: "It parses git-receive-pack command", desc: "It parses git-receive-pack command",
environment: map[string]string{ environment: map[string]string{
...@@ -57,7 +57,7 @@ func TestParseSuccess(t *testing.T) { ...@@ -57,7 +57,7 @@ func TestParseSuccess(t *testing.T) {
"SSH_ORIGINAL_COMMAND": "git-receive-pack group/repo", "SSH_ORIGINAL_COMMAND": "git-receive-pack group/repo",
}, },
arguments: []string{string(GitlabShell)}, arguments: []string{string(GitlabShell)},
expectedArgs: &CommandArgs{arguments: []string{string(GitlabShell)}, SshArgs: []string{"git-receive-pack", "group/repo"}, CommandType: ReceivePack}, expectedArgs: &Shell{BaseArgs: &BaseArgs{arguments: []string{string(GitlabShell)}}, SshArgs: []string{"git-receive-pack", "group/repo"}, CommandType: ReceivePack},
}, { }, {
desc: "It parses git-receive-pack command and a project with single quotes", desc: "It parses git-receive-pack command and a project with single quotes",
environment: map[string]string{ environment: map[string]string{
...@@ -65,7 +65,7 @@ func TestParseSuccess(t *testing.T) { ...@@ -65,7 +65,7 @@ func TestParseSuccess(t *testing.T) {
"SSH_ORIGINAL_COMMAND": "git receive-pack 'group/repo'", "SSH_ORIGINAL_COMMAND": "git receive-pack 'group/repo'",
}, },
arguments: []string{string(GitlabShell)}, arguments: []string{string(GitlabShell)},
expectedArgs: &CommandArgs{arguments: []string{string(GitlabShell)}, SshArgs: []string{"git-receive-pack", "group/repo"}, CommandType: ReceivePack}, expectedArgs: &Shell{BaseArgs: &BaseArgs{arguments: []string{string(GitlabShell)}}, SshArgs: []string{"git-receive-pack", "group/repo"}, CommandType: ReceivePack},
}, { }, {
desc: `It parses "git receive-pack" command`, desc: `It parses "git receive-pack" command`,
environment: map[string]string{ environment: map[string]string{
...@@ -73,7 +73,7 @@ func TestParseSuccess(t *testing.T) { ...@@ -73,7 +73,7 @@ func TestParseSuccess(t *testing.T) {
"SSH_ORIGINAL_COMMAND": `git receive-pack "group/repo"`, "SSH_ORIGINAL_COMMAND": `git receive-pack "group/repo"`,
}, },
arguments: []string{string(GitlabShell)}, arguments: []string{string(GitlabShell)},
expectedArgs: &CommandArgs{arguments: []string{string(GitlabShell)}, SshArgs: []string{"git-receive-pack", "group/repo"}, CommandType: ReceivePack}, expectedArgs: &Shell{BaseArgs: &BaseArgs{arguments: []string{string(GitlabShell)}}, SshArgs: []string{"git-receive-pack", "group/repo"}, CommandType: ReceivePack},
}, { }, {
desc: `It parses a command followed by control characters`, desc: `It parses a command followed by control characters`,
environment: map[string]string{ environment: map[string]string{
...@@ -81,7 +81,7 @@ func TestParseSuccess(t *testing.T) { ...@@ -81,7 +81,7 @@ func TestParseSuccess(t *testing.T) {
"SSH_ORIGINAL_COMMAND": `git-receive-pack group/repo; any command`, "SSH_ORIGINAL_COMMAND": `git-receive-pack group/repo; any command`,
}, },
arguments: []string{string(GitlabShell)}, arguments: []string{string(GitlabShell)},
expectedArgs: &CommandArgs{arguments: []string{string(GitlabShell)}, SshArgs: []string{"git-receive-pack", "group/repo"}, CommandType: ReceivePack}, expectedArgs: &Shell{BaseArgs: &BaseArgs{arguments: []string{string(GitlabShell)}}, SshArgs: []string{"git-receive-pack", "group/repo"}, CommandType: ReceivePack},
}, { }, {
desc: "It parses git-upload-pack command", desc: "It parses git-upload-pack command",
environment: map[string]string{ environment: map[string]string{
...@@ -89,7 +89,7 @@ func TestParseSuccess(t *testing.T) { ...@@ -89,7 +89,7 @@ func TestParseSuccess(t *testing.T) {
"SSH_ORIGINAL_COMMAND": `git upload-pack "group/repo"`, "SSH_ORIGINAL_COMMAND": `git upload-pack "group/repo"`,
}, },
arguments: []string{string(GitlabShell)}, arguments: []string{string(GitlabShell)},
expectedArgs: &CommandArgs{arguments: []string{string(GitlabShell)}, SshArgs: []string{"git-upload-pack", "group/repo"}, CommandType: UploadPack}, expectedArgs: &Shell{BaseArgs: &BaseArgs{arguments: []string{string(GitlabShell)}}, SshArgs: []string{"git-upload-pack", "group/repo"}, CommandType: UploadPack},
}, { }, {
desc: "It parses git-upload-archive command", desc: "It parses git-upload-archive command",
environment: map[string]string{ environment: map[string]string{
...@@ -97,7 +97,7 @@ func TestParseSuccess(t *testing.T) { ...@@ -97,7 +97,7 @@ func TestParseSuccess(t *testing.T) {
"SSH_ORIGINAL_COMMAND": "git-upload-archive 'group/repo'", "SSH_ORIGINAL_COMMAND": "git-upload-archive 'group/repo'",
}, },
arguments: []string{string(GitlabShell)}, arguments: []string{string(GitlabShell)},
expectedArgs: &CommandArgs{arguments: []string{string(GitlabShell)}, SshArgs: []string{"git-upload-archive", "group/repo"}, CommandType: UploadArchive}, expectedArgs: &Shell{BaseArgs: &BaseArgs{arguments: []string{string(GitlabShell)}}, SshArgs: []string{"git-upload-archive", "group/repo"}, CommandType: UploadArchive},
}, { }, {
desc: "It parses git-lfs-authenticate command", desc: "It parses git-lfs-authenticate command",
environment: map[string]string{ environment: map[string]string{
...@@ -105,7 +105,11 @@ func TestParseSuccess(t *testing.T) { ...@@ -105,7 +105,11 @@ func TestParseSuccess(t *testing.T) {
"SSH_ORIGINAL_COMMAND": "git-lfs-authenticate 'group/repo' download", "SSH_ORIGINAL_COMMAND": "git-lfs-authenticate 'group/repo' download",
}, },
arguments: []string{string(GitlabShell)}, arguments: []string{string(GitlabShell)},
expectedArgs: &CommandArgs{arguments: []string{string(GitlabShell)}, SshArgs: []string{"git-lfs-authenticate", "group/repo", "download"}, CommandType: LfsAuthenticate}, expectedArgs: &Shell{BaseArgs: &BaseArgs{arguments: []string{string(GitlabShell)}}, SshArgs: []string{"git-lfs-authenticate", "group/repo", "download"}, CommandType: LfsAuthenticate},
}, {
desc: "Unknown executable",
arguments: []string{"unknown"},
expectedArgs: &BaseArgs{arguments: []string{"unknown"}},
}, },
} }
...@@ -123,22 +127,41 @@ func TestParseSuccess(t *testing.T) { ...@@ -123,22 +127,41 @@ func TestParseSuccess(t *testing.T) {
} }
func TestParseFailure(t *testing.T) { func TestParseFailure(t *testing.T) {
t.Run("It fails if SSH connection is not set", func(t *testing.T) { testCases := []struct {
_, err := Parse([]string{string(GitlabShell)}) desc string
environment map[string]string
require.Error(t, err, "Only ssh allowed") arguments []string
}) expectedError string
}{
{
desc: "It fails if SSH connection is not set",
arguments: []string{string(GitlabShell)},
expectedError: "Only ssh allowed",
},
{
desc: "It fails if SSH command is invalid",
environment: map[string]string{
"SSH_CONNECTION": "1",
"SSH_ORIGINAL_COMMAND": `git receive-pack "`,
},
arguments: []string{string(GitlabShell)},
expectedError: "Only ssh allowed",
},
{
desc: "It fails if arguments is empty",
arguments: []string{},
expectedError: "arguments should include the executable",
},
}
t.Run("It fails if SSH command is invalid", func(t *testing.T) { for _, tc := range testCases {
environment := map[string]string{ t.Run(tc.desc, func(t *testing.T) {
"SSH_CONNECTION": "1", restoreEnv := testhelper.TempEnv(tc.environment)
"SSH_ORIGINAL_COMMAND": `git receive-pack "`, defer restoreEnv()
}
restoreEnv := testhelper.TempEnv(environment)
defer restoreEnv()
_, err := Parse([]string{string(GitlabShell)}) _, err := Parse(tc.arguments)
require.Error(t, err, "Invalid ssh command") require.Error(t, err, tc.expectedError)
}) })
}
} }
package commandargs
import (
"errors"
"os"
"regexp"
"github.com/mattn/go-shellwords"
)
const (
Discover CommandType = "discover"
TwoFactorRecover CommandType = "2fa_recovery_codes"
LfsAuthenticate CommandType = "git-lfs-authenticate"
ReceivePack CommandType = "git-receive-pack"
UploadPack CommandType = "git-upload-pack"
UploadArchive CommandType = "git-upload-archive"
)
var (
whoKeyRegex = regexp.MustCompile(`\bkey-(?P<keyid>\d+)\b`)
whoUsernameRegex = regexp.MustCompile(`\busername-(?P<username>\S+)\b`)
)
type Shell struct {
*BaseArgs
GitlabUsername string
GitlabKeyId string
SshArgs []string
CommandType CommandType
}
func (s *Shell) Parse() error {
if sshConnection := os.Getenv("SSH_CONNECTION"); sshConnection == "" {
return errors.New("Only ssh allowed")
}
s.parseWho()
if err := s.parseCommand(os.Getenv("SSH_ORIGINAL_COMMAND")); err != nil {
return errors.New("Invalid ssh command")
}
s.defineCommandType()
return nil
}
func (s *Shell) parseWho() {
for _, argument := range s.arguments {
if keyId := tryParseKeyId(argument); keyId != "" {
s.GitlabKeyId = keyId
break
}
if username := tryParseUsername(argument); username != "" {
s.GitlabUsername = username
break
}
}
}
func tryParseKeyId(argument string) string {
matchInfo := whoKeyRegex.FindStringSubmatch(argument)
if len(matchInfo) == 2 {
// The first element is the full matched string
// The second element is the named `keyid`
return matchInfo[1]
}
return ""
}
func tryParseUsername(argument string) string {
matchInfo := whoUsernameRegex.FindStringSubmatch(argument)
if len(matchInfo) == 2 {
// The first element is the full matched string
// The second element is the named `username`
return matchInfo[1]
}
return ""
}
func (s *Shell) parseCommand(commandString string) error {
args, err := shellwords.Parse(commandString)
if err != nil {
return err
}
// Handle Git for Windows 2.14 using "git upload-pack" instead of git-upload-pack
if len(args) > 1 && args[0] == "git" {
command := args[0] + "-" + args[1]
commandArgs := args[2:]
args = append([]string{command}, commandArgs...)
}
s.SshArgs = args
return nil
}
func (s *Shell) defineCommandType() {
if len(s.SshArgs) == 0 {
s.CommandType = Discover
} else {
s.CommandType = CommandType(s.SshArgs[0])
}
}
...@@ -9,14 +9,9 @@ import ( ...@@ -9,14 +9,9 @@ import (
"gitlab.com/gitlab-org/gitlab-shell/go/internal/command/commandargs" "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/commandargs"
) )
type CommandArgs interface {
Executable() commandargs.Executable
Arguments() []string
}
type Command struct { type Command struct {
RootDir string RootDir string
Args CommandArgs Args commandargs.CommandArgs
} }
var ( var (
......
...@@ -25,6 +25,11 @@ type FakeCommandArgs struct { ...@@ -25,6 +25,11 @@ type FakeCommandArgs struct {
arguments []string arguments []string
} }
func (f *FakeCommandArgs) Parse() error {
// Do nothing as no need to parse anything
return nil
}
func (f *FakeCommandArgs) Executable() commandargs.Executable { func (f *FakeCommandArgs) Executable() commandargs.Executable {
return f.executable return f.executable
} }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment