Commit c577eb9e authored by Ash McKenzie's avatar Ash McKenzie

Merge branch '181-migrate-gitlab-shell-checks-fallback' into 'master'

Support falling back to ruby version of checkers

See merge request gitlab-org/gitlab-shell!318
parents ed046037 3b6f9f75
...@@ -16,8 +16,14 @@ test_ruby: ...@@ -16,8 +16,14 @@ test_ruby:
# bin/gitlab-shell must exist and needs to be the Ruby version for # bin/gitlab-shell must exist and needs to be the Ruby version for
# rspec to be able to test. # rspec to be able to test.
cp bin/gitlab-shell-ruby bin/gitlab-shell cp bin/gitlab-shell-ruby bin/gitlab-shell
# bin/gitlab-shell-authorized-keys-check and bin/gitlab-shell-authorized-principals-check
# should link to ruby scripts for rspec to be able to test.
ln -sf ./gitlab-shell-authorized-keys-check-ruby bin/gitlab-shell-authorized-keys-check
ln -sf ./gitlab-shell-authorized-principals-check-ruby bin/gitlab-shell-authorized-principals-check
bundle exec rspec --color --tag '~go' --format d spec bundle exec rspec --color --tag '~go' --format d spec
rm -f bin/gitlab-shell rm -f bin/gitlab-shell
ln -sf ./gitlab-shell bin/gitlab-shell-authorized-keys-check
ln -sf ./gitlab-shell bin/gitlab-shell-authorized-principals-check
test_golang: test_golang:
support/go-test support/go-test
......
#!/usr/bin/env ruby
#
# GitLab shell authorized_keys helper. Query GitLab API to get the authorized
# command for a given ssh key fingerprint
#
# Ex.
# bin/gitlab-shell-authorized-keys-check <username> <public-key>
#
# Returns
# command="/bin/gitlab-shell key-#",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-rsa AAAA...
#
# Expects to be called by the SSH daemon, via configuration like:
# AuthorizedKeysCommandUser git
# AuthorizedKeysCommand /bin/gitlab-shell-authorized-keys-check git %u %k
abort "# Wrong number of arguments. #{ARGV.size}. Usage:
# gitlab-shell-authorized-keys-check <expected-username> <actual-username> <key>" unless ARGV.size == 3
expected_username = ARGV[0]
abort '# No username provided' if expected_username.nil? || expected_username == ''
actual_username = ARGV[1]
abort '# No username provided' if actual_username.nil? || actual_username == ''
# Only check access if the requested username matches the configured username.
# Normally, these would both be 'git', but it can be configured by the user
exit 0 unless expected_username == actual_username
key = ARGV[2]
abort "# No key provided" if key.nil? || key == ''
require_relative '../lib/gitlab_init'
require_relative '../lib/gitlab_net'
require_relative '../lib/gitlab_keys'
authorized_key = GitlabNet.new.authorized_key(key)
if authorized_key.nil?
puts "# No key was found for #{key}"
else
puts GitlabKeys.key_line("key-#{authorized_key['id']}", authorized_key['key'])
end
./gitlab-shell
\ No newline at end of file
#!/usr/bin/env ruby
#
# GitLab shell authorized_keys helper. Query GitLab API to get the authorized
# command for a given ssh key fingerprint
#
# Ex.
# bin/gitlab-shell-authorized-keys-check <username> <public-key>
#
# Returns
# command="/bin/gitlab-shell key-#",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-rsa AAAA...
#
# Expects to be called by the SSH daemon, via configuration like:
# AuthorizedKeysCommandUser git
# AuthorizedKeysCommand /bin/gitlab-shell-authorized-keys-check git %u %k
abort "# Wrong number of arguments. #{ARGV.size}. Usage:
# gitlab-shell-authorized-keys-check <expected-username> <actual-username> <key>" unless ARGV.size == 3
expected_username = ARGV[0]
abort '# No username provided' if expected_username.nil? || expected_username == ''
actual_username = ARGV[1]
abort '# No username provided' if actual_username.nil? || actual_username == ''
# Only check access if the requested username matches the configured username.
# Normally, these would both be 'git', but it can be configured by the user
exit 0 unless expected_username == actual_username
key = ARGV[2]
abort "# No key provided" if key.nil? || key == ''
require_relative '../lib/gitlab_init'
require_relative '../lib/gitlab_net'
require_relative '../lib/gitlab_keys'
authorized_key = GitlabNet.new.authorized_key(key)
if authorized_key.nil?
puts "# No key was found for #{key}"
else
puts GitlabKeys.key_line("key-#{authorized_key['id']}", authorized_key['key'])
end
#!/usr/bin/env ruby
#
# GitLab shell authorized principals helper. Emits the same sort of
# command="..." line as gitlab-shell-authorized-principals-check, with
# the right options.
#
# Ex.
# bin/gitlab-shell-authorized-keys-check <key-id> <principal1> [<principal2>...]
#
# Returns one line per principal passed in, e.g.:
# command="/bin/gitlab-shell username-{KEY_ID}",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty {PRINCIPAL}
# [command="/bin/gitlab-shell username-{KEY_ID}",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty {PRINCIPAL2}]
#
# Expects to be called by the SSH daemon, via configuration like:
# AuthorizedPrincipalsCommandUser root
# AuthorizedPrincipalsCommand /bin/gitlab-shell-authorized-principals-check git %i sshUsers
abort "# Wrong number of arguments. #{ARGV.size}. Usage:
# gitlab-shell-authorized-principals-check <key-id> <principal1> [<principal2>...]" unless ARGV.size >= 2
key_id = ARGV[0]
abort '# No key_id provided' if key_id.nil? || key_id == ''
principals = ARGV[1..-1]
principals.each { |principal|
abort '# An invalid principal was provided' if principal.nil? || principal == ''
}
require_relative '../lib/gitlab_init'
require_relative '../lib/gitlab_net'
require_relative '../lib/gitlab_keys'
principals.each { |principal|
puts GitlabKeys.principal_line("username-#{key_id}", principal.dup)
}
./gitlab-shell
\ No newline at end of file
#!/usr/bin/env ruby
#
# GitLab shell authorized principals helper. Emits the same sort of
# command="..." line as gitlab-shell-authorized-principals-check, with
# the right options.
#
# Ex.
# bin/gitlab-shell-authorized-keys-check <key-id> <principal1> [<principal2>...]
#
# Returns one line per principal passed in, e.g.:
# command="/bin/gitlab-shell username-{KEY_ID}",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty {PRINCIPAL}
# [command="/bin/gitlab-shell username-{KEY_ID}",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty {PRINCIPAL2}]
#
# Expects to be called by the SSH daemon, via configuration like:
# AuthorizedPrincipalsCommandUser root
# AuthorizedPrincipalsCommand /bin/gitlab-shell-authorized-principals-check git %i sshUsers
abort "# Wrong number of arguments. #{ARGV.size}. Usage:
# gitlab-shell-authorized-principals-check <key-id> <principal1> [<principal2>...]" unless ARGV.size >= 2
key_id = ARGV[0]
abort '# No key_id provided' if key_id.nil? || key_id == ''
principals = ARGV[1..-1]
principals.each { |principal|
abort '# An invalid principal was provided' if principal.nil? || principal == ''
}
require_relative '../lib/gitlab_init'
require_relative '../lib/gitlab_net'
require_relative '../lib/gitlab_keys'
principals.each { |principal|
puts GitlabKeys.principal_line("username-#{key_id}", principal.dup)
}
...@@ -3,42 +3,13 @@ package main ...@@ -3,42 +3,13 @@ package main
import ( import (
"fmt" "fmt"
"os" "os"
"path/filepath"
"gitlab.com/gitlab-org/gitlab-shell/go/internal/command" "gitlab.com/gitlab-org/gitlab-shell/go/internal/command"
"gitlab.com/gitlab-org/gitlab-shell/go/internal/command/fallback"
"gitlab.com/gitlab-org/gitlab-shell/go/internal/command/readwriter" "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/readwriter"
"gitlab.com/gitlab-org/gitlab-shell/go/internal/config" "gitlab.com/gitlab-org/gitlab-shell/go/internal/config"
"gitlab.com/gitlab-org/gitlab-shell/go/internal/executable"
) )
// findRootDir determines the root directory (and so, the location of the config
// file) from os.Executable()
func findRootDir() (string, error) {
if path := os.Getenv("GITLAB_SHELL_DIR"); path != "" {
return path, nil
}
path, err := os.Executable()
if err != nil {
return "", err
}
// Start: /opt/.../gitlab-shell/bin/gitlab-shell
// Ends: /opt/.../gitlab-shell
return filepath.Dir(filepath.Dir(path)), nil
}
// rubyExec will never return. It either replaces the current process with a
// Ruby interpreter, or outputs an error and kills the process.
func execRuby(rootDir string, readWriter *readwriter.ReadWriter) {
cmd := &fallback.Command{RootDir: rootDir, Args: os.Args}
if err := cmd.Execute(); err != nil {
fmt.Fprintf(readWriter.ErrOut, "Failed to exec: %v\n", err)
os.Exit(1)
}
}
func main() { func main() {
readWriter := &readwriter.ReadWriter{ readWriter := &readwriter.ReadWriter{
Out: os.Stdout, Out: os.Stdout,
...@@ -46,21 +17,19 @@ func main() { ...@@ -46,21 +17,19 @@ func main() {
ErrOut: os.Stderr, ErrOut: os.Stderr,
} }
rootDir, err := findRootDir() executable, err := executable.New()
if err != nil { if err != nil {
fmt.Fprintln(readWriter.ErrOut, "Failed to determine root directory, exiting") fmt.Fprintln(readWriter.ErrOut, "Failed to determine executable, exiting")
os.Exit(1) os.Exit(1)
} }
// Fall back to Ruby in case of problems reading the config, but issue a config, err := config.NewFromDir(executable.RootDir)
// warning as this isn't something we can sustain indefinitely
config, err := config.NewFromDir(rootDir)
if err != nil { if err != nil {
fmt.Fprintln(readWriter.ErrOut, "Failed to read config, falling back to gitlab-shell-ruby") fmt.Fprintln(readWriter.ErrOut, "Failed to read config, exiting")
execRuby(rootDir, readWriter) os.Exit(1)
} }
cmd, err := command.New(os.Args, config, readWriter) cmd, err := command.New(executable, os.Args[1:], config, readWriter)
if err != nil { if err != nil {
// For now this could happen if `SSH_CONNECTION` is not set on // For now this could happen if `SSH_CONNECTION` is not set on
// the environment // the environment
......
...@@ -11,29 +11,40 @@ import ( ...@@ -11,29 +11,40 @@ import (
"gitlab.com/gitlab-org/gitlab-shell/go/internal/command/uploadarchive" "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/uploadarchive"
"gitlab.com/gitlab-org/gitlab-shell/go/internal/command/uploadpack" "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/uploadpack"
"gitlab.com/gitlab-org/gitlab-shell/go/internal/config" "gitlab.com/gitlab-org/gitlab-shell/go/internal/config"
"gitlab.com/gitlab-org/gitlab-shell/go/internal/executable"
) )
type Command interface { type Command interface {
Execute() error Execute() error
} }
func New(arguments []string, config *config.Config, readWriter *readwriter.ReadWriter) (Command, error) { func New(e *executable.Executable, arguments []string, config *config.Config, readWriter *readwriter.ReadWriter) (Command, error) {
args, err := commandargs.Parse(arguments) args, err := commandargs.Parse(e, arguments)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if config.FeatureEnabled(string(args.CommandType)) { if cmd := buildCommand(e, args, config, readWriter); cmd != nil {
if cmd := buildCommand(args, config, readWriter); cmd != nil { return cmd, nil
return cmd, nil }
}
return &fallback.Command{Executable: e, RootDir: config.RootDir, Args: args}, nil
}
func buildCommand(e *executable.Executable, args commandargs.CommandArgs, config *config.Config, readWriter *readwriter.ReadWriter) Command {
switch e.Name {
case executable.GitlabShell:
return buildShellCommand(args.(*commandargs.Shell), config, readWriter)
} }
return &fallback.Command{RootDir: config.RootDir, Args: arguments}, nil return nil
} }
func buildCommand(args *commandargs.CommandArgs, config *config.Config, readWriter *readwriter.ReadWriter) Command { 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}
......
...@@ -13,18 +13,22 @@ import ( ...@@ -13,18 +13,22 @@ import (
"gitlab.com/gitlab-org/gitlab-shell/go/internal/command/uploadarchive" "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/uploadarchive"
"gitlab.com/gitlab-org/gitlab-shell/go/internal/command/uploadpack" "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/uploadpack"
"gitlab.com/gitlab-org/gitlab-shell/go/internal/config" "gitlab.com/gitlab-org/gitlab-shell/go/internal/config"
"gitlab.com/gitlab-org/gitlab-shell/go/internal/executable"
"gitlab.com/gitlab-org/gitlab-shell/go/internal/testhelper" "gitlab.com/gitlab-org/gitlab-shell/go/internal/testhelper"
) )
func TestNew(t *testing.T) { func TestNew(t *testing.T) {
testCases := []struct { testCases := []struct {
desc string desc string
executable *executable.Executable
config *config.Config config *config.Config
environment map[string]string environment map[string]string
arguments []string
expectedType interface{} expectedType interface{}
}{ }{
{ {
desc: "it returns a Discover command if the feature is enabled", desc: "it returns a Discover command if the feature is enabled",
executable: &executable.Executable{Name: executable.GitlabShell},
config: &config.Config{ config: &config.Config{
GitlabUrl: "http+unix://gitlab.socket", GitlabUrl: "http+unix://gitlab.socket",
Migration: config.MigrationConfig{Enabled: true, Features: []string{"discover"}}, Migration: config.MigrationConfig{Enabled: true, Features: []string{"discover"}},
...@@ -33,10 +37,12 @@ func TestNew(t *testing.T) { ...@@ -33,10 +37,12 @@ func TestNew(t *testing.T) {
"SSH_CONNECTION": "1", "SSH_CONNECTION": "1",
"SSH_ORIGINAL_COMMAND": "", "SSH_ORIGINAL_COMMAND": "",
}, },
arguments: []string{},
expectedType: &discover.Command{}, expectedType: &discover.Command{},
}, },
{ {
desc: "it returns a Fallback command no feature is enabled", desc: "it returns a Fallback command no feature is enabled",
executable: &executable.Executable{Name: executable.GitlabShell},
config: &config.Config{ config: &config.Config{
GitlabUrl: "http+unix://gitlab.socket", GitlabUrl: "http+unix://gitlab.socket",
Migration: config.MigrationConfig{Enabled: false}, Migration: config.MigrationConfig{Enabled: false},
...@@ -45,10 +51,12 @@ func TestNew(t *testing.T) { ...@@ -45,10 +51,12 @@ func TestNew(t *testing.T) {
"SSH_CONNECTION": "1", "SSH_CONNECTION": "1",
"SSH_ORIGINAL_COMMAND": "", "SSH_ORIGINAL_COMMAND": "",
}, },
arguments: []string{},
expectedType: &fallback.Command{}, expectedType: &fallback.Command{},
}, },
{ {
desc: "it returns a TwoFactorRecover command if the feature is enabled", desc: "it returns a TwoFactorRecover command if the feature is enabled",
executable: &executable.Executable{Name: executable.GitlabShell},
config: &config.Config{ config: &config.Config{
GitlabUrl: "http+unix://gitlab.socket", GitlabUrl: "http+unix://gitlab.socket",
Migration: config.MigrationConfig{Enabled: true, Features: []string{"2fa_recovery_codes"}}, Migration: config.MigrationConfig{Enabled: true, Features: []string{"2fa_recovery_codes"}},
...@@ -57,10 +65,12 @@ func TestNew(t *testing.T) { ...@@ -57,10 +65,12 @@ func TestNew(t *testing.T) {
"SSH_CONNECTION": "1", "SSH_CONNECTION": "1",
"SSH_ORIGINAL_COMMAND": "2fa_recovery_codes", "SSH_ORIGINAL_COMMAND": "2fa_recovery_codes",
}, },
arguments: []string{},
expectedType: &twofactorrecover.Command{}, expectedType: &twofactorrecover.Command{},
}, },
{ {
desc: "it returns an LfsAuthenticate command if the feature is enabled", desc: "it returns an LfsAuthenticate command if the feature is enabled",
executable: &executable.Executable{Name: executable.GitlabShell},
config: &config.Config{ config: &config.Config{
GitlabUrl: "http+unix://gitlab.socket", GitlabUrl: "http+unix://gitlab.socket",
Migration: config.MigrationConfig{Enabled: true, Features: []string{"git-lfs-authenticate"}}, Migration: config.MigrationConfig{Enabled: true, Features: []string{"git-lfs-authenticate"}},
...@@ -69,10 +79,12 @@ func TestNew(t *testing.T) { ...@@ -69,10 +79,12 @@ func TestNew(t *testing.T) {
"SSH_CONNECTION": "1", "SSH_CONNECTION": "1",
"SSH_ORIGINAL_COMMAND": "git-lfs-authenticate", "SSH_ORIGINAL_COMMAND": "git-lfs-authenticate",
}, },
arguments: []string{},
expectedType: &lfsauthenticate.Command{}, expectedType: &lfsauthenticate.Command{},
}, },
{ {
desc: "it returns a ReceivePack command if the feature is enabled", desc: "it returns a ReceivePack command if the feature is enabled",
executable: &executable.Executable{Name: executable.GitlabShell},
config: &config.Config{ config: &config.Config{
GitlabUrl: "http+unix://gitlab.socket", GitlabUrl: "http+unix://gitlab.socket",
Migration: config.MigrationConfig{Enabled: true, Features: []string{"git-receive-pack"}}, Migration: config.MigrationConfig{Enabled: true, Features: []string{"git-receive-pack"}},
...@@ -81,10 +93,12 @@ func TestNew(t *testing.T) { ...@@ -81,10 +93,12 @@ func TestNew(t *testing.T) {
"SSH_CONNECTION": "1", "SSH_CONNECTION": "1",
"SSH_ORIGINAL_COMMAND": "git-receive-pack", "SSH_ORIGINAL_COMMAND": "git-receive-pack",
}, },
arguments: []string{},
expectedType: &receivepack.Command{}, expectedType: &receivepack.Command{},
}, },
{ {
desc: "it returns a UploadPack command if the feature is enabled", desc: "it returns an UploadPack command if the feature is enabled",
executable: &executable.Executable{Name: executable.GitlabShell},
config: &config.Config{ config: &config.Config{
GitlabUrl: "http+unix://gitlab.socket", GitlabUrl: "http+unix://gitlab.socket",
Migration: config.MigrationConfig{Enabled: true, Features: []string{"git-upload-pack"}}, Migration: config.MigrationConfig{Enabled: true, Features: []string{"git-upload-pack"}},
...@@ -93,10 +107,12 @@ func TestNew(t *testing.T) { ...@@ -93,10 +107,12 @@ func TestNew(t *testing.T) {
"SSH_CONNECTION": "1", "SSH_CONNECTION": "1",
"SSH_ORIGINAL_COMMAND": "git-upload-pack", "SSH_ORIGINAL_COMMAND": "git-upload-pack",
}, },
arguments: []string{},
expectedType: &uploadpack.Command{}, expectedType: &uploadpack.Command{},
}, },
{ {
desc: "it returns a UploadArchive command if the feature is enabled", desc: "it returns an UploadArchive command if the feature is enabled",
executable: &executable.Executable{Name: executable.GitlabShell},
config: &config.Config{ config: &config.Config{
GitlabUrl: "http+unix://gitlab.socket", GitlabUrl: "http+unix://gitlab.socket",
Migration: config.MigrationConfig{Enabled: true, Features: []string{"git-upload-archive"}}, Migration: config.MigrationConfig{Enabled: true, Features: []string{"git-upload-archive"}},
...@@ -105,10 +121,12 @@ func TestNew(t *testing.T) { ...@@ -105,10 +121,12 @@ func TestNew(t *testing.T) {
"SSH_CONNECTION": "1", "SSH_CONNECTION": "1",
"SSH_ORIGINAL_COMMAND": "git-upload-archive", "SSH_ORIGINAL_COMMAND": "git-upload-archive",
}, },
arguments: []string{},
expectedType: &uploadarchive.Command{}, expectedType: &uploadarchive.Command{},
}, },
{ {
desc: "it returns a Fallback command if the feature is unimplemented", desc: "it returns a Fallback command if the feature is unimplemented",
executable: &executable.Executable{Name: executable.GitlabShell},
config: &config.Config{ config: &config.Config{
GitlabUrl: "http+unix://gitlab.socket", GitlabUrl: "http+unix://gitlab.socket",
Migration: config.MigrationConfig{Enabled: true, Features: []string{"git-unimplemented-feature"}}, Migration: config.MigrationConfig{Enabled: true, Features: []string{"git-unimplemented-feature"}},
...@@ -117,6 +135,14 @@ func TestNew(t *testing.T) { ...@@ -117,6 +135,14 @@ func TestNew(t *testing.T) {
"SSH_CONNECTION": "1", "SSH_CONNECTION": "1",
"SSH_ORIGINAL_COMMAND": "git-unimplemented-feature", "SSH_ORIGINAL_COMMAND": "git-unimplemented-feature",
}, },
arguments: []string{},
expectedType: &fallback.Command{},
},
{
desc: "it returns a Fallback command if executable is unknown",
executable: &executable.Executable{Name: "unknown"},
config: &config.Config{},
arguments: []string{},
expectedType: &fallback.Command{}, expectedType: &fallback.Command{},
}, },
} }
...@@ -126,7 +152,7 @@ func TestNew(t *testing.T) { ...@@ -126,7 +152,7 @@ func TestNew(t *testing.T) {
restoreEnv := testhelper.TempEnv(tc.environment) restoreEnv := testhelper.TempEnv(tc.environment)
defer restoreEnv() defer restoreEnv()
command, err := New([]string{}, tc.config, nil) command, err := New(tc.executable, tc.arguments, tc.config, nil)
require.NoError(t, err) require.NoError(t, err)
require.IsType(t, tc.expectedType, command) require.IsType(t, tc.expectedType, command)
...@@ -135,12 +161,9 @@ func TestNew(t *testing.T) { ...@@ -135,12 +161,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(&executable.Executable{Name: executable.GitlabShell}, []string{}, &config.Config{}, nil)
defer restoreEnv()
_, err := New([]string{}, &config.Config{}, nil)
require.Error(t, err, "Only ssh allowed") require.Error(t, err)
}) })
} }
package commandargs package commandargs
import ( import (
"errors" "gitlab.com/gitlab-org/gitlab-shell/go/internal/executable"
"os"
"regexp"
"github.com/mattn/go-shellwords"
) )
type CommandType string type CommandType string
const ( type CommandArgs interface {
Discover CommandType = "discover" Parse() error
TwoFactorRecover CommandType = "2fa_recovery_codes" GetArguments() []string
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 CommandArgs struct {
GitlabUsername string
GitlabKeyId string
SshArgs []string
CommandType CommandType
}
func Parse(arguments []string) (*CommandArgs, error) {
if sshConnection := os.Getenv("SSH_CONNECTION"); sshConnection == "" {
return nil, errors.New("Only ssh allowed")
}
args := &CommandArgs{}
args.parseWho(arguments)
if err := args.parseCommand(os.Getenv("SSH_ORIGINAL_COMMAND")); err != nil {
return nil, errors.New("Invalid ssh command")
}
args.defineCommandType()
return args, nil
}
func (c *CommandArgs) parseWho(arguments []string) {
for _, argument := range arguments {
if keyId := tryParseKeyId(argument); keyId != "" {
c.GitlabKeyId = keyId
break
}
if username := tryParseUsername(argument); username != "" {
c.GitlabUsername = username
break
}
}
} }
func tryParseKeyId(argument string) string { func Parse(e *executable.Executable, arguments []string) (CommandArgs, error) {
matchInfo := whoKeyRegex.FindStringSubmatch(argument) var args CommandArgs = &GenericArgs{Arguments: arguments}
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 { switch e.Name {
matchInfo := whoUsernameRegex.FindStringSubmatch(argument) case executable.GitlabShell:
if len(matchInfo) == 2 { args = &Shell{Arguments: arguments}
// The first element is the full matched string
// The second element is the named `username`
return matchInfo[1]
} }
return "" if err := args.Parse(); err != nil {
} return nil, err
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 return args, nil
if len(args) > 1 && args[0] == "git" {
command := args[0] + "-" + args[1]
commandArgs := args[2:]
args = append([]string{command}, commandArgs...)
}
c.SshArgs = args
return nil
}
func (c *CommandArgs) defineCommandType() {
if len(c.SshArgs) == 0 {
c.CommandType = Discover
} else {
c.CommandType = CommandType(c.SshArgs[0])
}
} }
...@@ -3,100 +3,127 @@ package commandargs ...@@ -3,100 +3,127 @@ package commandargs
import ( import (
"testing" "testing"
"github.com/stretchr/testify/require" "gitlab.com/gitlab-org/gitlab-shell/go/internal/executable"
"gitlab.com/gitlab-org/gitlab-shell/go/internal/testhelper" "gitlab.com/gitlab-org/gitlab-shell/go/internal/testhelper"
"github.com/stretchr/testify/require"
) )
func TestParseSuccess(t *testing.T) { func TestParseSuccess(t *testing.T) {
testCases := []struct { testCases := []struct {
desc string desc string
arguments []string executable *executable.Executable
environment map[string]string environment map[string]string
expectedArgs *CommandArgs arguments []string
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.
{ {
desc: "It sets discover as the command when the command string was empty", desc: "It sets discover as the command when the command string was empty",
executable: &executable.Executable{Name: executable.GitlabShell},
environment: map[string]string{ environment: map[string]string{
"SSH_CONNECTION": "1", "SSH_CONNECTION": "1",
"SSH_ORIGINAL_COMMAND": "", "SSH_ORIGINAL_COMMAND": "",
}, },
expectedArgs: &CommandArgs{SshArgs: []string{}, CommandType: Discover}, arguments: []string{},
expectedArgs: &Shell{Arguments: []string{}, SshArgs: []string{}, CommandType: Discover},
}, },
{ {
desc: "It finds the key id in any passed arguments", desc: "It finds the key id in any passed arguments",
executable: &executable.Executable{Name: executable.GitlabShell},
environment: map[string]string{ environment: map[string]string{
"SSH_CONNECTION": "1", "SSH_CONNECTION": "1",
"SSH_ORIGINAL_COMMAND": "", "SSH_ORIGINAL_COMMAND": "",
}, },
arguments: []string{"hello", "key-123"}, arguments: []string{"hello", "key-123"},
expectedArgs: &CommandArgs{SshArgs: []string{}, CommandType: Discover, GitlabKeyId: "123"}, expectedArgs: &Shell{Arguments: []string{"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",
executable: &executable.Executable{Name: executable.GitlabShell},
environment: map[string]string{ environment: map[string]string{
"SSH_CONNECTION": "1", "SSH_CONNECTION": "1",
"SSH_ORIGINAL_COMMAND": "", "SSH_ORIGINAL_COMMAND": "",
}, },
arguments: []string{"hello", "username-jane-doe"}, arguments: []string{"hello", "username-jane-doe"},
expectedArgs: &CommandArgs{SshArgs: []string{}, CommandType: Discover, GitlabUsername: "jane-doe"}, expectedArgs: &Shell{Arguments: []string{"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",
executable: &executable.Executable{Name: executable.GitlabShell},
environment: map[string]string{ environment: map[string]string{
"SSH_CONNECTION": "1", "SSH_CONNECTION": "1",
"SSH_ORIGINAL_COMMAND": "2fa_recovery_codes", "SSH_ORIGINAL_COMMAND": "2fa_recovery_codes",
}, },
expectedArgs: &CommandArgs{SshArgs: []string{"2fa_recovery_codes"}, CommandType: TwoFactorRecover}, arguments: []string{},
expectedArgs: &Shell{Arguments: []string{}, SshArgs: []string{"2fa_recovery_codes"}, CommandType: TwoFactorRecover},
}, { }, {
desc: "It parses git-receive-pack command", desc: "It parses git-receive-pack command",
executable: &executable.Executable{Name: executable.GitlabShell},
environment: map[string]string{ environment: map[string]string{
"SSH_CONNECTION": "1", "SSH_CONNECTION": "1",
"SSH_ORIGINAL_COMMAND": "git-receive-pack group/repo", "SSH_ORIGINAL_COMMAND": "git-receive-pack group/repo",
}, },
expectedArgs: &CommandArgs{SshArgs: []string{"git-receive-pack", "group/repo"}, CommandType: ReceivePack}, arguments: []string{},
expectedArgs: &Shell{Arguments: []string{}, 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",
executable: &executable.Executable{Name: executable.GitlabShell},
environment: map[string]string{ environment: map[string]string{
"SSH_CONNECTION": "1", "SSH_CONNECTION": "1",
"SSH_ORIGINAL_COMMAND": "git receive-pack 'group/repo'", "SSH_ORIGINAL_COMMAND": "git receive-pack 'group/repo'",
}, },
expectedArgs: &CommandArgs{SshArgs: []string{"git-receive-pack", "group/repo"}, CommandType: ReceivePack}, arguments: []string{},
expectedArgs: &Shell{Arguments: []string{}, SshArgs: []string{"git-receive-pack", "group/repo"}, CommandType: ReceivePack},
}, { }, {
desc: `It parses "git receive-pack" command`, desc: `It parses "git receive-pack" command`,
executable: &executable.Executable{Name: executable.GitlabShell},
environment: map[string]string{ environment: map[string]string{
"SSH_CONNECTION": "1", "SSH_CONNECTION": "1",
"SSH_ORIGINAL_COMMAND": `git receive-pack "group/repo"`, "SSH_ORIGINAL_COMMAND": `git receive-pack "group/repo"`,
}, },
expectedArgs: &CommandArgs{SshArgs: []string{"git-receive-pack", "group/repo"}, CommandType: ReceivePack}, arguments: []string{},
expectedArgs: &Shell{Arguments: []string{}, 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`,
executable: &executable.Executable{Name: executable.GitlabShell},
environment: map[string]string{ environment: map[string]string{
"SSH_CONNECTION": "1", "SSH_CONNECTION": "1",
"SSH_ORIGINAL_COMMAND": `git-receive-pack group/repo; any command`, "SSH_ORIGINAL_COMMAND": `git-receive-pack group/repo; any command`,
}, },
expectedArgs: &CommandArgs{SshArgs: []string{"git-receive-pack", "group/repo"}, CommandType: ReceivePack}, arguments: []string{},
expectedArgs: &Shell{Arguments: []string{}, SshArgs: []string{"git-receive-pack", "group/repo"}, CommandType: ReceivePack},
}, { }, {
desc: "It parses git-upload-pack command", desc: "It parses git-upload-pack command",
executable: &executable.Executable{Name: executable.GitlabShell},
environment: map[string]string{ environment: map[string]string{
"SSH_CONNECTION": "1", "SSH_CONNECTION": "1",
"SSH_ORIGINAL_COMMAND": `git upload-pack "group/repo"`, "SSH_ORIGINAL_COMMAND": `git upload-pack "group/repo"`,
}, },
expectedArgs: &CommandArgs{SshArgs: []string{"git-upload-pack", "group/repo"}, CommandType: UploadPack}, arguments: []string{},
expectedArgs: &Shell{Arguments: []string{}, SshArgs: []string{"git-upload-pack", "group/repo"}, CommandType: UploadPack},
}, { }, {
desc: "It parses git-upload-archive command", desc: "It parses git-upload-archive command",
executable: &executable.Executable{Name: executable.GitlabShell},
environment: map[string]string{ environment: map[string]string{
"SSH_CONNECTION": "1", "SSH_CONNECTION": "1",
"SSH_ORIGINAL_COMMAND": "git-upload-archive 'group/repo'", "SSH_ORIGINAL_COMMAND": "git-upload-archive 'group/repo'",
}, },
expectedArgs: &CommandArgs{SshArgs: []string{"git-upload-archive", "group/repo"}, CommandType: UploadArchive}, arguments: []string{},
expectedArgs: &Shell{Arguments: []string{}, SshArgs: []string{"git-upload-archive", "group/repo"}, CommandType: UploadArchive},
}, { }, {
desc: "It parses git-lfs-authenticate command", desc: "It parses git-lfs-authenticate command",
executable: &executable.Executable{Name: executable.GitlabShell},
environment: map[string]string{ environment: map[string]string{
"SSH_CONNECTION": "1", "SSH_CONNECTION": "1",
"SSH_ORIGINAL_COMMAND": "git-lfs-authenticate 'group/repo' download", "SSH_ORIGINAL_COMMAND": "git-lfs-authenticate 'group/repo' download",
}, },
expectedArgs: &CommandArgs{SshArgs: []string{"git-lfs-authenticate", "group/repo", "download"}, CommandType: LfsAuthenticate}, arguments: []string{},
expectedArgs: &Shell{Arguments: []string{}, SshArgs: []string{"git-lfs-authenticate", "group/repo", "download"}, CommandType: LfsAuthenticate},
}, {
desc: "Unknown executable",
executable: &executable.Executable{Name: "unknown"},
arguments: []string{},
expectedArgs: &GenericArgs{Arguments: []string{}},
}, },
} }
...@@ -105,7 +132,7 @@ func TestParseSuccess(t *testing.T) { ...@@ -105,7 +132,7 @@ func TestParseSuccess(t *testing.T) {
restoreEnv := testhelper.TempEnv(tc.environment) restoreEnv := testhelper.TempEnv(tc.environment)
defer restoreEnv() defer restoreEnv()
result, err := Parse(tc.arguments) result, err := Parse(tc.executable, tc.arguments)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, tc.expectedArgs, result) require.Equal(t, tc.expectedArgs, result)
...@@ -114,22 +141,39 @@ func TestParseSuccess(t *testing.T) { ...@@ -114,22 +141,39 @@ 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{}) desc string
executable *executable.Executable
require.Error(t, err, "Only ssh allowed") environment map[string]string
}) arguments []string
expectedError string
}{
{
desc: "It fails if SSH connection is not set",
executable: &executable.Executable{Name: executable.GitlabShell},
arguments: []string{},
expectedError: "Only SSH allowed",
},
{
desc: "It fails if SSH command is invalid",
executable: &executable.Executable{Name: executable.GitlabShell},
environment: map[string]string{
"SSH_CONNECTION": "1",
"SSH_ORIGINAL_COMMAND": `git receive-pack "`,
},
arguments: []string{},
expectedError: "Invalid SSH allowed",
},
}
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{}) _, err := Parse(tc.executable, tc.arguments)
require.Error(t, err, "Invalid ssh command") require.Error(t, err, tc.expectedError)
}) })
}
} }
package commandargs
type GenericArgs struct {
Arguments []string
}
func (b *GenericArgs) Parse() error {
// Do nothing
return nil
}
func (b *GenericArgs) GetArguments() []string {
return b.Arguments
}
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 {
Arguments []string
GitlabUsername string
GitlabKeyId string
SshArgs []string
CommandType CommandType
}
func (s *Shell) Parse() error {
if err := s.validate(); err != nil {
return err
}
s.parseWho()
s.defineCommandType()
return nil
}
func (s *Shell) GetArguments() []string {
return s.Arguments
}
func (s *Shell) validate() error {
if !s.isSshConnection() {
return errors.New("Only SSH allowed")
}
if !s.isValidSshCommand() {
return errors.New("Invalid SSH command")
}
return nil
}
func (s *Shell) isSshConnection() bool {
ok := os.Getenv("SSH_CONNECTION")
return ok != ""
}
func (s *Shell) isValidSshCommand() bool {
err := s.parseCommand(os.Getenv("SSH_ORIGINAL_COMMAND"))
return err == 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])
}
}
...@@ -11,7 +11,7 @@ import ( ...@@ -11,7 +11,7 @@ import (
type Command struct { type Command struct {
Config *config.Config Config *config.Config
Args *commandargs.CommandArgs Args *commandargs.Shell
ReadWriter *readwriter.ReadWriter ReadWriter *readwriter.ReadWriter
} }
......
...@@ -49,27 +49,27 @@ func TestExecute(t *testing.T) { ...@@ -49,27 +49,27 @@ func TestExecute(t *testing.T) {
testCases := []struct { testCases := []struct {
desc string desc string
arguments *commandargs.CommandArgs arguments *commandargs.Shell
expectedOutput string expectedOutput string
}{ }{
{ {
desc: "With a known username", desc: "With a known username",
arguments: &commandargs.CommandArgs{GitlabUsername: "alex-doe"}, arguments: &commandargs.Shell{GitlabUsername: "alex-doe"},
expectedOutput: "Welcome to GitLab, @alex-doe!\n", expectedOutput: "Welcome to GitLab, @alex-doe!\n",
}, },
{ {
desc: "With a known key id", desc: "With a known key id",
arguments: &commandargs.CommandArgs{GitlabKeyId: "1"}, arguments: &commandargs.Shell{GitlabKeyId: "1"},
expectedOutput: "Welcome to GitLab, @alex-doe!\n", expectedOutput: "Welcome to GitLab, @alex-doe!\n",
}, },
{ {
desc: "With an unknown key", desc: "With an unknown key",
arguments: &commandargs.CommandArgs{GitlabKeyId: "-1"}, arguments: &commandargs.Shell{GitlabKeyId: "-1"},
expectedOutput: "Welcome to GitLab, Anonymous!\n", expectedOutput: "Welcome to GitLab, Anonymous!\n",
}, },
{ {
desc: "With an unknown username", desc: "With an unknown username",
arguments: &commandargs.CommandArgs{GitlabUsername: "unknown"}, arguments: &commandargs.Shell{GitlabUsername: "unknown"},
expectedOutput: "Welcome to GitLab, Anonymous!\n", expectedOutput: "Welcome to GitLab, Anonymous!\n",
}, },
} }
...@@ -97,22 +97,22 @@ func TestFailingExecute(t *testing.T) { ...@@ -97,22 +97,22 @@ func TestFailingExecute(t *testing.T) {
testCases := []struct { testCases := []struct {
desc string desc string
arguments *commandargs.CommandArgs arguments *commandargs.Shell
expectedError string expectedError string
}{ }{
{ {
desc: "With missing arguments", desc: "With missing arguments",
arguments: &commandargs.CommandArgs{}, arguments: &commandargs.Shell{},
expectedError: "Failed to get username: who='' is invalid", expectedError: "Failed to get username: who='' is invalid",
}, },
{ {
desc: "When the API returns an error", desc: "When the API returns an error",
arguments: &commandargs.CommandArgs{GitlabUsername: "broken_message"}, arguments: &commandargs.Shell{GitlabUsername: "broken_message"},
expectedError: "Failed to get username: Forbidden!", expectedError: "Failed to get username: Forbidden!",
}, },
{ {
desc: "When the API fails", desc: "When the API fails",
arguments: &commandargs.CommandArgs{GitlabUsername: "broken"}, arguments: &commandargs.Shell{GitlabUsername: "broken"},
expectedError: "Failed to get username: Internal API error (500)", expectedError: "Failed to get username: Internal API error (500)",
}, },
} }
......
package fallback package fallback
import ( import (
"errors"
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"syscall" "syscall"
"gitlab.com/gitlab-org/gitlab-shell/go/internal/command/commandargs"
"gitlab.com/gitlab-org/gitlab-shell/go/internal/executable"
) )
type Command struct { type Command struct {
RootDir string Executable *executable.Executable
Args []string RootDir string
Args commandargs.CommandArgs
} }
var ( var (
// execFunc is overridden in tests // execFunc is overridden in tests
execFunc = syscall.Exec execFunc = syscall.Exec
) whitelist = []string{
executable.GitlabShell,
const ( executable.AuthorizedKeysCheck,
RubyProgram = "gitlab-shell-ruby" executable.AuthorizedPrincipalsCheck,
}
) )
func (c *Command) Execute() error { func (c *Command) Execute() error {
rubyCmd := filepath.Join(c.RootDir, "bin", RubyProgram) if !c.isWhitelisted() {
return errors.New("Failed to execute unknown executable")
}
rubyCmd := c.fallbackProgram()
// Ensure rubyArgs[0] is the full path to gitlab-shell-ruby // Ensure rubyArgs[0] is the full path to gitlab-shell-ruby
rubyArgs := append([]string{rubyCmd}, c.Args[1:]...) rubyArgs := append([]string{rubyCmd}, c.Args.GetArguments()...)
return execFunc(rubyCmd, rubyArgs, os.Environ()) return execFunc(rubyCmd, rubyArgs, os.Environ())
} }
func (c *Command) isWhitelisted() bool {
for _, item := range whitelist {
if c.Executable.Name == item {
return true
}
}
return false
}
func (c *Command) fallbackProgram() string {
fileName := fmt.Sprintf("%s-ruby", c.Executable.Name)
return filepath.Join(c.RootDir, "bin", fileName)
}
...@@ -6,6 +6,9 @@ import ( ...@@ -6,6 +6,9 @@ import (
"testing" "testing"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"gitlab.com/gitlab-org/gitlab-shell/go/internal/command/commandargs"
"gitlab.com/gitlab-org/gitlab-shell/go/internal/executable"
) )
type fakeExec struct { type fakeExec struct {
...@@ -19,7 +22,7 @@ type fakeExec struct { ...@@ -19,7 +22,7 @@ type fakeExec struct {
} }
var ( var (
fakeArgs = []string{"./test", "foo", "bar"} fakeArgs = &commandargs.GenericArgs{Arguments: []string{"foo", "bar"}}
) )
func (f *fakeExec) Exec(filename string, args []string, env []string) error { func (f *fakeExec) Exec(filename string, args []string, env []string) error {
...@@ -42,7 +45,7 @@ func (f *fakeExec) Cleanup() { ...@@ -42,7 +45,7 @@ func (f *fakeExec) Cleanup() {
} }
func TestExecuteExecsCommandSuccesfully(t *testing.T) { func TestExecuteExecsCommandSuccesfully(t *testing.T) {
cmd := &Command{RootDir: "/tmp", Args: fakeArgs} cmd := &Command{Executable: &executable.Executable{Name: executable.GitlabShell}, RootDir: "/tmp", Args: fakeArgs}
// Override the exec func // Override the exec func
fake := &fakeExec{} fake := &fakeExec{}
...@@ -56,8 +59,14 @@ func TestExecuteExecsCommandSuccesfully(t *testing.T) { ...@@ -56,8 +59,14 @@ func TestExecuteExecsCommandSuccesfully(t *testing.T) {
require.Equal(t, fake.Env, os.Environ()) require.Equal(t, fake.Env, os.Environ())
} }
func TestExecuteExecsUnknownExecutable(t *testing.T) {
cmd := &Command{Executable: &executable.Executable{Name: "unknown"}, RootDir: "/test"}
require.Error(t, cmd.Execute())
}
func TestExecuteExecsCommandOnError(t *testing.T) { func TestExecuteExecsCommandOnError(t *testing.T) {
cmd := &Command{RootDir: "/test", Args: fakeArgs} cmd := &Command{Executable: &executable.Executable{Name: executable.GitlabShell}, RootDir: "/test", Args: fakeArgs}
// Override the exec func // Override the exec func
fake := &fakeExec{Error: errors.New("Test error")} fake := &fakeExec{Error: errors.New("Test error")}
...@@ -69,7 +78,7 @@ func TestExecuteExecsCommandOnError(t *testing.T) { ...@@ -69,7 +78,7 @@ func TestExecuteExecsCommandOnError(t *testing.T) {
} }
func TestExecuteGivenNonexistentCommand(t *testing.T) { func TestExecuteGivenNonexistentCommand(t *testing.T) {
cmd := &Command{RootDir: "/tmp/does/not/exist", Args: fakeArgs} cmd := &Command{Executable: &executable.Executable{Name: executable.GitlabShell}, RootDir: "/tmp/does/not/exist", Args: fakeArgs}
require.Error(t, cmd.Execute()) require.Error(t, cmd.Execute())
} }
...@@ -20,7 +20,7 @@ const ( ...@@ -20,7 +20,7 @@ const (
type Command struct { type Command struct {
Config *config.Config Config *config.Config
Args *commandargs.CommandArgs Args *commandargs.Shell
ReadWriter *readwriter.ReadWriter ReadWriter *readwriter.ReadWriter
} }
......
...@@ -25,22 +25,22 @@ func TestFailedRequests(t *testing.T) { ...@@ -25,22 +25,22 @@ func TestFailedRequests(t *testing.T) {
testCases := []struct { testCases := []struct {
desc string desc string
arguments *commandargs.CommandArgs arguments *commandargs.Shell
expectedOutput string expectedOutput string
}{ }{
{ {
desc: "With missing arguments", desc: "With missing arguments",
arguments: &commandargs.CommandArgs{}, arguments: &commandargs.Shell{},
expectedOutput: "> GitLab: Disallowed command", expectedOutput: "> GitLab: Disallowed command",
}, },
{ {
desc: "With disallowed command", desc: "With disallowed command",
arguments: &commandargs.CommandArgs{GitlabKeyId: "1", SshArgs: []string{"git-lfs-authenticate", "group/repo", "unknown"}}, arguments: &commandargs.Shell{GitlabKeyId: "1", SshArgs: []string{"git-lfs-authenticate", "group/repo", "unknown"}},
expectedOutput: "> GitLab: Disallowed command", expectedOutput: "> GitLab: Disallowed command",
}, },
{ {
desc: "With disallowed user", desc: "With disallowed user",
arguments: &commandargs.CommandArgs{GitlabKeyId: "disallowed", SshArgs: []string{"git-lfs-authenticate", "group/repo", "download"}}, arguments: &commandargs.Shell{GitlabKeyId: "disallowed", SshArgs: []string{"git-lfs-authenticate", "group/repo", "download"}},
expectedOutput: "Disallowed by API call", expectedOutput: "Disallowed by API call",
}, },
} }
...@@ -140,7 +140,7 @@ func TestLfsAuthenticateRequests(t *testing.T) { ...@@ -140,7 +140,7 @@ func TestLfsAuthenticateRequests(t *testing.T) {
output := &bytes.Buffer{} output := &bytes.Buffer{}
cmd := &Command{ cmd := &Command{
Config: &config.Config{GitlabUrl: url}, Config: &config.Config{GitlabUrl: url},
Args: &commandargs.CommandArgs{GitlabUsername: tc.username, SshArgs: []string{"git-lfs-authenticate", "group/repo", "upload"}}, Args: &commandargs.Shell{GitlabUsername: tc.username, SshArgs: []string{"git-lfs-authenticate", "group/repo", "upload"}},
ReadWriter: &readwriter.ReadWriter{ErrOut: output, Out: output}, ReadWriter: &readwriter.ReadWriter{ErrOut: output, Out: output},
} }
......
...@@ -92,7 +92,7 @@ func TestCustomReceivePack(t *testing.T) { ...@@ -92,7 +92,7 @@ func TestCustomReceivePack(t *testing.T) {
cmd := &Command{ cmd := &Command{
Config: &config.Config{GitlabUrl: url}, Config: &config.Config{GitlabUrl: url},
Args: &commandargs.CommandArgs{GitlabKeyId: keyId, CommandType: commandargs.ReceivePack, SshArgs: []string{"git-receive-pack", repo}}, Args: &commandargs.Shell{GitlabKeyId: keyId, CommandType: commandargs.ReceivePack, SshArgs: []string{"git-receive-pack", repo}},
ReadWriter: &readwriter.ReadWriter{ErrOut: errBuf, Out: outBuf, In: input}, ReadWriter: &readwriter.ReadWriter{ErrOut: errBuf, Out: outBuf, In: input},
} }
......
...@@ -29,7 +29,7 @@ func TestReceivePack(t *testing.T) { ...@@ -29,7 +29,7 @@ func TestReceivePack(t *testing.T) {
cmd := &Command{ cmd := &Command{
Config: &config.Config{GitlabUrl: url}, Config: &config.Config{GitlabUrl: url},
Args: &commandargs.CommandArgs{GitlabKeyId: userId, CommandType: commandargs.ReceivePack, SshArgs: []string{"git-receive-pack", repo}}, Args: &commandargs.Shell{GitlabKeyId: userId, CommandType: commandargs.ReceivePack, SshArgs: []string{"git-receive-pack", repo}},
ReadWriter: &readwriter.ReadWriter{ErrOut: output, Out: output, In: input}, ReadWriter: &readwriter.ReadWriter{ErrOut: output, Out: output, In: input},
} }
......
...@@ -10,7 +10,7 @@ import ( ...@@ -10,7 +10,7 @@ import (
type Command struct { type Command struct {
Config *config.Config Config *config.Config
Args *commandargs.CommandArgs Args *commandargs.Shell
ReadWriter *readwriter.ReadWriter ReadWriter *readwriter.ReadWriter
} }
......
...@@ -23,7 +23,7 @@ func TestForbiddenAccess(t *testing.T) { ...@@ -23,7 +23,7 @@ func TestForbiddenAccess(t *testing.T) {
cmd := &Command{ cmd := &Command{
Config: &config.Config{GitlabUrl: url}, Config: &config.Config{GitlabUrl: url},
Args: &commandargs.CommandArgs{GitlabKeyId: "disallowed", SshArgs: []string{"git-receive-pack", "group/repo"}}, Args: &commandargs.Shell{GitlabKeyId: "disallowed", SshArgs: []string{"git-receive-pack", "group/repo"}},
ReadWriter: &readwriter.ReadWriter{ErrOut: output, Out: output, In: input}, ReadWriter: &readwriter.ReadWriter{ErrOut: output, Out: output, In: input},
} }
......
...@@ -14,7 +14,7 @@ type Response = accessverifier.Response ...@@ -14,7 +14,7 @@ type Response = accessverifier.Response
type Command struct { type Command struct {
Config *config.Config Config *config.Config
Args *commandargs.CommandArgs Args *commandargs.Shell
ReadWriter *readwriter.ReadWriter ReadWriter *readwriter.ReadWriter
} }
......
...@@ -64,7 +64,7 @@ func TestMissingUser(t *testing.T) { ...@@ -64,7 +64,7 @@ func TestMissingUser(t *testing.T) {
cmd, _, _, cleanup := setup(t) cmd, _, _, cleanup := setup(t)
defer cleanup() defer cleanup()
cmd.Args = &commandargs.CommandArgs{GitlabKeyId: "2"} cmd.Args = &commandargs.Shell{GitlabKeyId: "2"}
_, err := cmd.Verify(action, repo) _, err := cmd.Verify(action, repo)
require.Equal(t, "missing user", err.Error()) require.Equal(t, "missing user", err.Error())
...@@ -74,7 +74,7 @@ func TestConsoleMessages(t *testing.T) { ...@@ -74,7 +74,7 @@ func TestConsoleMessages(t *testing.T) {
cmd, errBuf, outBuf, cleanup := setup(t) cmd, errBuf, outBuf, cleanup := setup(t)
defer cleanup() defer cleanup()
cmd.Args = &commandargs.CommandArgs{GitlabKeyId: "1"} cmd.Args = &commandargs.Shell{GitlabKeyId: "1"}
cmd.Verify(action, repo) cmd.Verify(action, repo)
require.Equal(t, "> GitLab: console\n> GitLab: message\n", errBuf.String()) require.Equal(t, "> GitLab: console\n> GitLab: message\n", errBuf.String())
......
...@@ -12,7 +12,7 @@ import ( ...@@ -12,7 +12,7 @@ import (
type Command struct { type Command struct {
Config *config.Config Config *config.Config
Args *commandargs.CommandArgs Args *commandargs.Shell
ReadWriter *readwriter.ReadWriter ReadWriter *readwriter.ReadWriter
} }
......
...@@ -69,13 +69,13 @@ func TestExecute(t *testing.T) { ...@@ -69,13 +69,13 @@ func TestExecute(t *testing.T) {
testCases := []struct { testCases := []struct {
desc string desc string
arguments *commandargs.CommandArgs arguments *commandargs.Shell
answer string answer string
expectedOutput string expectedOutput string
}{ }{
{ {
desc: "With a known key id", desc: "With a known key id",
arguments: &commandargs.CommandArgs{GitlabKeyId: "1"}, arguments: &commandargs.Shell{GitlabKeyId: "1"},
answer: "yes\n", answer: "yes\n",
expectedOutput: question + expectedOutput: question +
"Your two-factor authentication recovery codes are:\n\nrecovery\ncodes\n\n" + "Your two-factor authentication recovery codes are:\n\nrecovery\ncodes\n\n" +
...@@ -85,31 +85,31 @@ func TestExecute(t *testing.T) { ...@@ -85,31 +85,31 @@ func TestExecute(t *testing.T) {
}, },
{ {
desc: "With bad response", desc: "With bad response",
arguments: &commandargs.CommandArgs{GitlabKeyId: "-1"}, arguments: &commandargs.Shell{GitlabKeyId: "-1"},
answer: "yes\n", answer: "yes\n",
expectedOutput: question + errorHeader + "Parsing failed\n", expectedOutput: question + errorHeader + "Parsing failed\n",
}, },
{ {
desc: "With API returns an error", desc: "With API returns an error",
arguments: &commandargs.CommandArgs{GitlabKeyId: "forbidden"}, arguments: &commandargs.Shell{GitlabKeyId: "forbidden"},
answer: "yes\n", answer: "yes\n",
expectedOutput: question + errorHeader + "Forbidden!\n", expectedOutput: question + errorHeader + "Forbidden!\n",
}, },
{ {
desc: "With API fails", desc: "With API fails",
arguments: &commandargs.CommandArgs{GitlabKeyId: "broken"}, arguments: &commandargs.Shell{GitlabKeyId: "broken"},
answer: "yes\n", answer: "yes\n",
expectedOutput: question + errorHeader + "Internal API error (500)\n", expectedOutput: question + errorHeader + "Internal API error (500)\n",
}, },
{ {
desc: "With missing arguments", desc: "With missing arguments",
arguments: &commandargs.CommandArgs{}, arguments: &commandargs.Shell{},
answer: "yes\n", answer: "yes\n",
expectedOutput: question + errorHeader + "who='' is invalid\n", expectedOutput: question + errorHeader + "who='' is invalid\n",
}, },
{ {
desc: "With negative answer", desc: "With negative answer",
arguments: &commandargs.CommandArgs{}, arguments: &commandargs.Shell{},
answer: "no\n", answer: "no\n",
expectedOutput: question + expectedOutput: question +
"New recovery codes have *not* been generated. Existing codes will remain valid.\n", "New recovery codes have *not* been generated. Existing codes will remain valid.\n",
......
...@@ -29,7 +29,7 @@ func TestUploadPack(t *testing.T) { ...@@ -29,7 +29,7 @@ func TestUploadPack(t *testing.T) {
cmd := &Command{ cmd := &Command{
Config: &config.Config{GitlabUrl: url}, Config: &config.Config{GitlabUrl: url},
Args: &commandargs.CommandArgs{GitlabKeyId: userId, CommandType: commandargs.UploadArchive, SshArgs: []string{"git-upload-archive", repo}}, Args: &commandargs.Shell{GitlabKeyId: userId, CommandType: commandargs.UploadArchive, SshArgs: []string{"git-upload-archive", repo}},
ReadWriter: &readwriter.ReadWriter{ErrOut: output, Out: output, In: input}, ReadWriter: &readwriter.ReadWriter{ErrOut: output, Out: output, In: input},
} }
......
...@@ -10,7 +10,7 @@ import ( ...@@ -10,7 +10,7 @@ import (
type Command struct { type Command struct {
Config *config.Config Config *config.Config
Args *commandargs.CommandArgs Args *commandargs.Shell
ReadWriter *readwriter.ReadWriter ReadWriter *readwriter.ReadWriter
} }
......
...@@ -22,7 +22,7 @@ func TestForbiddenAccess(t *testing.T) { ...@@ -22,7 +22,7 @@ func TestForbiddenAccess(t *testing.T) {
cmd := &Command{ cmd := &Command{
Config: &config.Config{GitlabUrl: url}, Config: &config.Config{GitlabUrl: url},
Args: &commandargs.CommandArgs{GitlabKeyId: "disallowed", SshArgs: []string{"git-upload-archive", "group/repo"}}, Args: &commandargs.Shell{GitlabKeyId: "disallowed", SshArgs: []string{"git-upload-archive", "group/repo"}},
ReadWriter: &readwriter.ReadWriter{ErrOut: output, Out: output}, ReadWriter: &readwriter.ReadWriter{ErrOut: output, Out: output},
} }
......
...@@ -29,7 +29,7 @@ func TestUploadPack(t *testing.T) { ...@@ -29,7 +29,7 @@ func TestUploadPack(t *testing.T) {
cmd := &Command{ cmd := &Command{
Config: &config.Config{GitlabUrl: url}, Config: &config.Config{GitlabUrl: url},
Args: &commandargs.CommandArgs{GitlabKeyId: userId, CommandType: commandargs.UploadPack, SshArgs: []string{"git-upload-pack", repo}}, Args: &commandargs.Shell{GitlabKeyId: userId, CommandType: commandargs.UploadPack, SshArgs: []string{"git-upload-pack", repo}},
ReadWriter: &readwriter.ReadWriter{ErrOut: output, Out: output, In: input}, ReadWriter: &readwriter.ReadWriter{ErrOut: output, Out: output, In: input},
} }
......
...@@ -10,7 +10,7 @@ import ( ...@@ -10,7 +10,7 @@ import (
type Command struct { type Command struct {
Config *config.Config Config *config.Config
Args *commandargs.CommandArgs Args *commandargs.Shell
ReadWriter *readwriter.ReadWriter ReadWriter *readwriter.ReadWriter
} }
......
...@@ -22,7 +22,7 @@ func TestForbiddenAccess(t *testing.T) { ...@@ -22,7 +22,7 @@ func TestForbiddenAccess(t *testing.T) {
cmd := &Command{ cmd := &Command{
Config: &config.Config{GitlabUrl: url}, Config: &config.Config{GitlabUrl: url},
Args: &commandargs.CommandArgs{GitlabKeyId: "disallowed", SshArgs: []string{"git-upload-pack", "group/repo"}}, Args: &commandargs.Shell{GitlabKeyId: "disallowed", SshArgs: []string{"git-upload-pack", "group/repo"}},
ReadWriter: &readwriter.ReadWriter{ErrOut: output, Out: output}, ReadWriter: &readwriter.ReadWriter{ErrOut: output, Out: output},
} }
......
package executable
import (
"os"
"path/filepath"
)
const (
GitlabShell = "gitlab-shell"
AuthorizedKeysCheck = "gitlab-shell-authorized-keys-check"
AuthorizedPrincipalsCheck = "gitlab-shell-authorized-principals-check"
)
type Executable struct {
Name string
RootDir string
}
var (
// osExecutable is overridden in tests
osExecutable = os.Executable
)
func New() (*Executable, error) {
path, err := osExecutable()
if err != nil {
return nil, err
}
rootDir, err := findRootDir(path)
if err != nil {
return nil, err
}
executable := &Executable{
Name: filepath.Base(path),
RootDir: rootDir,
}
return executable, nil
}
func findRootDir(path string) (string, error) {
// Start: /opt/.../gitlab-shell/bin/gitlab-shell
// Ends: /opt/.../gitlab-shell
rootDir := filepath.Dir(filepath.Dir(path))
pathFromEnv := os.Getenv("GITLAB_SHELL_DIR")
if pathFromEnv != "" {
if _, err := os.Stat(pathFromEnv); os.IsNotExist(err) {
return "", err
}
rootDir = pathFromEnv
}
return rootDir, nil
}
package executable
import (
"errors"
"testing"
"gitlab.com/gitlab-org/gitlab-shell/go/internal/testhelper"
"github.com/stretchr/testify/require"
)
type fakeOs struct {
OldExecutable func() (string, error)
Path string
Error error
}
func (f *fakeOs) Executable() (string, error) {
return f.Path, f.Error
}
func (f *fakeOs) Setup() {
f.OldExecutable = osExecutable
osExecutable = f.Executable
}
func (f *fakeOs) Cleanup() {
osExecutable = f.OldExecutable
}
func TestNewSuccess(t *testing.T) {
testCases := []struct {
desc string
fakeOs *fakeOs
environment map[string]string
expectedRootDir string
}{
{
desc: "GITLAB_SHELL_DIR env var is not defined",
fakeOs: &fakeOs{Path: "/tmp/bin/gitlab-shell"},
expectedRootDir: "/tmp",
},
{
desc: "GITLAB_SHELL_DIR env var is defined",
fakeOs: &fakeOs{Path: "/opt/bin/gitlab-shell"},
environment: map[string]string{
"GITLAB_SHELL_DIR": "/tmp",
},
expectedRootDir: "/tmp",
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
restoreEnv := testhelper.TempEnv(tc.environment)
defer restoreEnv()
fake := tc.fakeOs
fake.Setup()
defer fake.Cleanup()
result, err := New()
require.NoError(t, err)
require.Equal(t, result.Name, "gitlab-shell")
require.Equal(t, result.RootDir, tc.expectedRootDir)
})
}
}
func TestNewFailure(t *testing.T) {
testCases := []struct {
desc string
fakeOs *fakeOs
environment map[string]string
}{
{
desc: "failed to determine executable",
fakeOs: &fakeOs{Path: "", Error: errors.New("error")},
},
{
desc: "GITLAB_SHELL_DIR doesn't exist",
fakeOs: &fakeOs{Path: "/tmp/bin/gitlab-shell"},
environment: map[string]string{
"GITLAB_SHELL_DIR": "/tmp/non/existing/directory",
},
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
restoreEnv := testhelper.TempEnv(tc.environment)
defer restoreEnv()
fake := tc.fakeOs
fake.Setup()
defer fake.Cleanup()
_, err := New()
require.Error(t, err)
})
}
}
...@@ -71,7 +71,7 @@ func NewClient(config *config.Config) (*Client, error) { ...@@ -71,7 +71,7 @@ func NewClient(config *config.Config) (*Client, error) {
return &Client{client: client}, nil return &Client{client: client}, nil
} }
func (c *Client) Verify(args *commandargs.CommandArgs, action commandargs.CommandType, repo string) (*Response, error) { func (c *Client) Verify(args *commandargs.Shell, action commandargs.CommandType, repo string) (*Response, error) {
request := &Request{Action: action, Repo: repo, Protocol: protocol, Changes: anyChanges} request := &Request{Action: action, Repo: repo, Protocol: protocol, Changes: anyChanges}
if args.GitlabUsername != "" { if args.GitlabUsername != "" {
...@@ -89,7 +89,7 @@ func (c *Client) Verify(args *commandargs.CommandArgs, action commandargs.Comman ...@@ -89,7 +89,7 @@ func (c *Client) Verify(args *commandargs.CommandArgs, action commandargs.Comman
return parse(response, args) return parse(response, args)
} }
func parse(hr *http.Response, args *commandargs.CommandArgs) (*Response, error) { func parse(hr *http.Response, args *commandargs.Shell) (*Response, error) {
response := &Response{} response := &Response{}
if err := gitlabnet.ParseJSON(hr, response); err != nil { if err := gitlabnet.ParseJSON(hr, response); err != nil {
return nil, err return nil, err
......
...@@ -57,16 +57,16 @@ func TestSuccessfulResponses(t *testing.T) { ...@@ -57,16 +57,16 @@ func TestSuccessfulResponses(t *testing.T) {
testCases := []struct { testCases := []struct {
desc string desc string
args *commandargs.CommandArgs args *commandargs.Shell
who string who string
}{ }{
{ {
desc: "Provide key id within the request", desc: "Provide key id within the request",
args: &commandargs.CommandArgs{GitlabKeyId: "1"}, args: &commandargs.Shell{GitlabKeyId: "1"},
who: "key-1", who: "key-1",
}, { }, {
desc: "Provide username within the request", desc: "Provide username within the request",
args: &commandargs.CommandArgs{GitlabUsername: "first"}, args: &commandargs.Shell{GitlabUsername: "first"},
who: "user-1", who: "user-1",
}, },
} }
...@@ -86,7 +86,7 @@ func TestGetCustomAction(t *testing.T) { ...@@ -86,7 +86,7 @@ func TestGetCustomAction(t *testing.T) {
client, cleanup := setup(t) client, cleanup := setup(t)
defer cleanup() defer cleanup()
args := &commandargs.CommandArgs{GitlabUsername: "custom"} args := &commandargs.Shell{GitlabUsername: "custom"}
result, err := client.Verify(args, action, repo) result, err := client.Verify(args, action, repo)
require.NoError(t, err) require.NoError(t, err)
...@@ -134,7 +134,7 @@ func TestErrorResponses(t *testing.T) { ...@@ -134,7 +134,7 @@ 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.CommandArgs{GitlabKeyId: tc.fakeId} args := &commandargs.Shell{GitlabKeyId: tc.fakeId}
resp, err := client.Verify(args, action, repo) resp, err := client.Verify(args, action, repo)
require.EqualError(t, err, tc.expectedError) require.EqualError(t, err, tc.expectedError)
......
...@@ -30,7 +30,7 @@ func NewClient(config *config.Config) (*Client, error) { ...@@ -30,7 +30,7 @@ func NewClient(config *config.Config) (*Client, error) {
return &Client{config: config, client: client}, nil return &Client{config: config, client: client}, nil
} }
func (c *Client) GetByCommandArgs(args *commandargs.CommandArgs) (*Response, error) { func (c *Client) GetByCommandArgs(args *commandargs.Shell) (*Response, error) {
params := url.Values{} params := url.Values{}
if args.GitlabUsername != "" { if args.GitlabUsername != "" {
params.Add("username", args.GitlabUsername) params.Add("username", args.GitlabUsername)
......
...@@ -13,7 +13,7 @@ import ( ...@@ -13,7 +13,7 @@ import (
type Client struct { type Client struct {
config *config.Config config *config.Config
client *gitlabnet.GitlabClient client *gitlabnet.GitlabClient
args *commandargs.CommandArgs args *commandargs.Shell
} }
type Request struct { type Request struct {
...@@ -30,7 +30,7 @@ type Response struct { ...@@ -30,7 +30,7 @@ type Response struct {
ExpiresIn int `json:"expires_in"` ExpiresIn int `json:"expires_in"`
} }
func NewClient(config *config.Config, args *commandargs.CommandArgs) (*Client, error) { func NewClient(config *config.Config, args *commandargs.Shell) (*Client, error) {
client, err := gitlabnet.GetClient(config) client, err := gitlabnet.GetClient(config)
if err != nil { if err != nil {
return nil, fmt.Errorf("Error creating http client: %v", err) return nil, fmt.Errorf("Error creating http client: %v", err)
......
...@@ -59,22 +59,22 @@ func TestFailedRequests(t *testing.T) { ...@@ -59,22 +59,22 @@ func TestFailedRequests(t *testing.T) {
testCases := []struct { testCases := []struct {
desc string desc string
args *commandargs.CommandArgs args *commandargs.Shell
expectedOutput string expectedOutput string
}{ }{
{ {
desc: "With bad response", desc: "With bad response",
args: &commandargs.CommandArgs{GitlabKeyId: "-1", CommandType: commandargs.UploadPack}, args: &commandargs.Shell{GitlabKeyId: "-1", CommandType: commandargs.UploadPack},
expectedOutput: "Parsing failed", expectedOutput: "Parsing failed",
}, },
{ {
desc: "With API returns an error", desc: "With API returns an error",
args: &commandargs.CommandArgs{GitlabKeyId: "forbidden", CommandType: commandargs.UploadPack}, args: &commandargs.Shell{GitlabKeyId: "forbidden", CommandType: commandargs.UploadPack},
expectedOutput: "Internal API error (403)", expectedOutput: "Internal API error (403)",
}, },
{ {
desc: "With API fails", desc: "With API fails",
args: &commandargs.CommandArgs{GitlabKeyId: "broken", CommandType: commandargs.UploadPack}, args: &commandargs.Shell{GitlabKeyId: "broken", CommandType: commandargs.UploadPack},
expectedOutput: "Internal API error (500)", expectedOutput: "Internal API error (500)",
}, },
} }
...@@ -99,7 +99,7 @@ func TestSuccessfulRequests(t *testing.T) { ...@@ -99,7 +99,7 @@ func TestSuccessfulRequests(t *testing.T) {
url, cleanup := testserver.StartHttpServer(t, requests) url, cleanup := testserver.StartHttpServer(t, requests)
defer cleanup() defer cleanup()
args := &commandargs.CommandArgs{GitlabKeyId: keyId, CommandType: commandargs.LfsAuthenticate} args := &commandargs.Shell{GitlabKeyId: keyId, CommandType: commandargs.LfsAuthenticate}
client, err := NewClient(&config.Config{GitlabUrl: url}, args) client, err := NewClient(&config.Config{GitlabUrl: url}, args)
require.NoError(t, err) require.NoError(t, err)
......
...@@ -36,7 +36,7 @@ func NewClient(config *config.Config) (*Client, error) { ...@@ -36,7 +36,7 @@ func NewClient(config *config.Config) (*Client, error) {
return &Client{config: config, client: client}, nil return &Client{config: config, client: client}, nil
} }
func (c *Client) GetRecoveryCodes(args *commandargs.CommandArgs) ([]string, error) { func (c *Client) GetRecoveryCodes(args *commandargs.Shell) ([]string, error) {
requestBody, err := c.getRequestBody(args) requestBody, err := c.getRequestBody(args)
if err != nil { if err != nil {
...@@ -65,7 +65,7 @@ func parse(hr *http.Response) ([]string, error) { ...@@ -65,7 +65,7 @@ func parse(hr *http.Response) ([]string, error) {
return response.RecoveryCodes, nil return response.RecoveryCodes, nil
} }
func (c *Client) getRequestBody(args *commandargs.CommandArgs) (*RequestBody, error) { func (c *Client) getRequestBody(args *commandargs.Shell) (*RequestBody, error) {
client, err := discover.NewClient(c.config) client, err := discover.NewClient(c.config)
if err != nil { if err != nil {
......
...@@ -85,7 +85,7 @@ func TestGetRecoveryCodesByKeyId(t *testing.T) { ...@@ -85,7 +85,7 @@ func TestGetRecoveryCodesByKeyId(t *testing.T) {
client, cleanup := setup(t) client, cleanup := setup(t)
defer cleanup() defer cleanup()
args := &commandargs.CommandArgs{GitlabKeyId: "0"} args := &commandargs.Shell{GitlabKeyId: "0"}
result, err := client.GetRecoveryCodes(args) result, err := client.GetRecoveryCodes(args)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, []string{"recovery 1", "codes 1"}, result) assert.Equal(t, []string{"recovery 1", "codes 1"}, result)
...@@ -95,7 +95,7 @@ func TestGetRecoveryCodesByUsername(t *testing.T) { ...@@ -95,7 +95,7 @@ func TestGetRecoveryCodesByUsername(t *testing.T) {
client, cleanup := setup(t) client, cleanup := setup(t)
defer cleanup() defer cleanup()
args := &commandargs.CommandArgs{GitlabUsername: "jane-doe"} args := &commandargs.Shell{GitlabUsername: "jane-doe"}
result, err := client.GetRecoveryCodes(args) result, err := client.GetRecoveryCodes(args)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, []string{"recovery 2", "codes 2"}, result) assert.Equal(t, []string{"recovery 2", "codes 2"}, result)
...@@ -105,7 +105,7 @@ func TestMissingUser(t *testing.T) { ...@@ -105,7 +105,7 @@ func TestMissingUser(t *testing.T) {
client, cleanup := setup(t) client, cleanup := setup(t)
defer cleanup() defer cleanup()
args := &commandargs.CommandArgs{GitlabKeyId: "1"} args := &commandargs.Shell{GitlabKeyId: "1"}
_, err := client.GetRecoveryCodes(args) _, err := client.GetRecoveryCodes(args)
assert.Equal(t, "missing user", err.Error()) assert.Equal(t, "missing user", err.Error())
} }
...@@ -138,7 +138,7 @@ func TestErrorResponses(t *testing.T) { ...@@ -138,7 +138,7 @@ 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.CommandArgs{GitlabKeyId: tc.fakeId} args := &commandargs.Shell{GitlabKeyId: tc.fakeId}
resp, err := client.GetRecoveryCodes(args) resp, err := client.GetRecoveryCodes(args)
assert.EqualError(t, err, tc.expectedError) assert.EqualError(t, err, tc.expectedError)
......
...@@ -21,54 +21,84 @@ describe 'bin/gitlab-shell-authorized-keys-check' do ...@@ -21,54 +21,84 @@ describe 'bin/gitlab-shell-authorized-keys-check' do
end end
end end
before(:all) do
write_config(
"gitlab_url" => "http+unix://#{CGI.escape(tmp_socket_path)}",
)
end
let(:authorized_keys_check_path) { File.join(tmp_root_path, 'bin', 'gitlab-shell-authorized-keys-check') } let(:authorized_keys_check_path) { File.join(tmp_root_path, 'bin', 'gitlab-shell-authorized-keys-check') }
it 'succeeds when a valid key is given' do shared_examples 'authorized keys check' do
output, status = run! it 'succeeds when a valid key is given' do
output, status = run!
expect(output).to eq("command=\"#{gitlab_shell_path} key-1\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty known-rsa-key\n") expect(output).to eq("command=\"#{gitlab_shell_path} key-1\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty known-rsa-key\n")
expect(status).to be_success expect(status).to be_success
end end
it 'returns nothing when an unknown key is given' do it 'returns nothing when an unknown key is given' do
output, status = run!(key: 'unknown-key') output, status = run!(key: 'unknown-key')
expect(output).to eq("# No key was found for unknown-key\n") expect(output).to eq("# No key was found for unknown-key\n")
expect(status).to be_success expect(status).to be_success
end end
it' fails when not enough arguments are given' do
output, status = run!(key: nil)
it' fails when not enough arguments are given' do expect(output).to eq('')
output, status = run!(key: nil) expect(status).not_to be_success
end
expect(output).to eq('') it' fails when too many arguments are given' do
expect(status).not_to be_success output, status = run!(key: ['a', 'b'])
expect(output).to eq('')
expect(status).not_to be_success
end
it 'skips when run as the wrong user' do
output, status = run!(expected_user: 'unknown-user')
expect(output).to eq('')
expect(status).to be_success
end
it 'skips when the wrong users connects' do
output, status = run!(actual_user: 'unknown-user')
expect(output).to eq('')
expect(status).to be_success
end
end end
it' fails when too many arguments are given' do describe 'without go features' do
output, status = run!(key: ['a', 'b']) before(:all) do
write_config(
"gitlab_url" => "http+unix://#{CGI.escape(tmp_socket_path)}",
)
end
expect(output).to eq('') it_behaves_like 'authorized keys check'
expect(status).not_to be_success
end end
it 'skips when run as the wrong user' do describe 'without go features (via go)', :go do
output, status = run!(expected_user: 'unknown-user') before(:all) do
write_config(
"gitlab_url" => "http+unix://#{CGI.escape(tmp_socket_path)}",
)
end
expect(output).to eq('') it_behaves_like 'authorized keys check'
expect(status).to be_success
end end
it 'skips when the wrong users connects' do pending 'with the go authorized-keys-check feature', :go do
output, status = run!(actual_user: 'unknown-user') before(:all) do
write_config(
'gitlab_url' => "http+unix://#{CGI.escape(tmp_socket_path)}",
'migration' => {
'enabled' => true,
'features' => ['authorized-keys-check']
}
)
end
expect(output).to eq('') it_behaves_like 'authorized keys check'
expect(status).to be_success
end end
def run!(expected_user: 'git', actual_user: 'git', key: 'known-rsa-key') def run!(expected_user: 'git', actual_user: 'git', key: 'known-rsa-key')
......
require_relative 'spec_helper'
describe 'bin/gitlab-shell-authorized-principals-check' do
include_context 'gitlab shell'
def mock_server(server)
# Do nothing as we're not connecting to a server in this check.
end
let(:authorized_principals_check_path) { File.join(tmp_root_path, 'bin', 'gitlab-shell-authorized-principals-check') }
shared_examples 'authorized principals check' do
it 'succeeds when a valid principal is given' do
output, status = run!
expect(output).to eq("command=\"#{gitlab_shell_path} username-key\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty principal\n")
expect(status).to be_success
end
it 'fails when not enough arguments are given' do
output, status = run!(key_id: nil, principals: [])
expect(output).to eq('')
expect(status).not_to be_success
end
it 'fails when key_id is blank' do
output, status = run!(key_id: '')
expect(output).to eq('')
expect(status).not_to be_success
end
it 'fails when principals include an empty item' do
output, status = run!(principals: ['principal', ''])
expect(output).to eq('')
expect(status).not_to be_success
end
end
describe 'without go features' do
before(:all) do
write_config({})
end
it_behaves_like 'authorized principals check'
end
describe 'without go features (via go)', :go do
before(:all) do
write_config({})
end
it_behaves_like 'authorized principals check'
end
pending 'with the go authorized-principals-check feature', :go do
before(:all) do
write_config(
'migration' => {
'enabled' => true,
'features' => ['authorized-principals-check']
}
)
end
it_behaves_like 'authorized principals check'
end
def run!(key_id: 'key', principals: ['principal'])
cmd = [
authorized_principals_check_path,
key_id,
principals,
].flatten.compact
output = IO.popen(cmd, &:read)
[output, $?]
end
end
...@@ -123,10 +123,10 @@ describe 'bin/gitlab-shell' do ...@@ -123,10 +123,10 @@ describe 'bin/gitlab-shell' do
it_behaves_like 'results with keys' it_behaves_like 'results with keys'
it 'outputs "Only ssh allowed"' do it 'outputs "Only SSH allowed"' do
_, stderr, status = run!(["-c/usr/share/webapps/gitlab-shell/bin/gitlab-shell", "username-someuser"], env: {}) _, stderr, status = run!(["-c/usr/share/webapps/gitlab-shell/bin/gitlab-shell", "username-someuser"], env: {})
expect(stderr).to eq("Only ssh allowed\n") expect(stderr).to eq("Only SSH allowed\n")
expect(status).not_to be_success expect(status).not_to be_success
end end
......
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