Files
teleport/lib/srv/exec_linux_test.go
T

215 lines
6.2 KiB
Go

//go:build linux
// +build linux
/*
* Teleport
* Copyright (C) 2023 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package srv
import (
"fmt"
"os"
"os/exec"
"os/user"
"path/filepath"
"strconv"
"syscall"
"testing"
"time"
"github.com/gravitational/trace"
"github.com/stretchr/testify/require"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/teleport/lib/utils/host"
)
func TestOSCommandPrep(t *testing.T) {
utils.RequireRoot(t)
srv := newMockServer(t)
scx := newExecServerContext(t, srv)
// because CheckHomeDir now inspects access to the home directory as the actual user after a rexec,
// we need to setup a real, non-root user with a valid home directory in order for this test to
// exercise the correct paths
tempHome := t.TempDir()
require.NoError(t, os.Chmod(filepath.Dir(tempHome), 0777))
username := "test-os-command-prep"
scx.Identity.Login = username
_, err := host.UserAdd(username, nil, host.UserOpts{
Home: tempHome,
})
require.NoError(t, err)
t.Cleanup(func() {
// change homedir back so user deletion doesn't fail
changeHomeDir(t, username, tempHome)
_, err := host.UserDel(username)
require.NoError(t, err)
})
usr, err := user.Lookup(username)
require.NoError(t, err)
uid, err := strconv.Atoi(usr.Uid)
require.NoError(t, err)
require.NoError(t, os.Chown(tempHome, uid, -1))
expectedEnv := []string{
"LANG=en_US.UTF-8",
getDefaultEnvPath(usr.Uid, defaultLoginDefsPath),
fmt.Sprintf("HOME=%s", usr.HomeDir),
fmt.Sprintf("USER=%s", username),
"SHELL=/bin/sh",
"SSH_CLIENT=10.0.0.5 4817 3022",
"SSH_CONNECTION=10.0.0.5 4817 127.0.0.1 3022",
"TERM=xterm",
fmt.Sprintf("SSH_TTY=%v", scx.session.term.TTYName()),
"SSH_SESSION_ID=xxx",
"SSH_TELEPORT_HOST_UUID=testID",
"SSH_TELEPORT_CLUSTER_NAME=localhost",
"SSH_TELEPORT_USER=teleportUser",
}
// Empty command (simple shell).
execCmd, err := scx.ExecCommand()
require.NoError(t, err)
cmd, err := buildCommand(execCmd, usr, nil, nil)
require.NoError(t, err)
require.NotNil(t, cmd)
require.Equal(t, "/bin/sh", cmd.Path)
require.Equal(t, []string{"-sh"}, cmd.Args)
require.Equal(t, usr.HomeDir, cmd.Dir)
require.Equal(t, expectedEnv, cmd.Env)
require.Equal(t, syscall.SIGKILL, cmd.SysProcAttr.Pdeathsig)
// Non-empty command (exec a prog).
scx.execRequest.SetCommand("ls -lh /etc")
execCmd, err = scx.ExecCommand()
require.NoError(t, err)
cmd, err = buildCommand(execCmd, usr, nil, nil)
require.NoError(t, err)
require.NotNil(t, cmd)
require.Equal(t, "/bin/sh", cmd.Path)
require.Equal(t, []string{"/bin/sh", "-c", "ls -lh /etc"}, cmd.Args)
require.Equal(t, usr.HomeDir, cmd.Dir)
require.Equal(t, expectedEnv, cmd.Env)
require.Equal(t, syscall.SIGKILL, cmd.SysProcAttr.Pdeathsig)
// Command without args.
scx.execRequest.SetCommand("top")
execCmd, err = scx.ExecCommand()
require.NoError(t, err)
cmd, err = buildCommand(execCmd, usr, nil, nil)
require.NoError(t, err)
require.Equal(t, "/bin/sh", cmd.Path)
require.Equal(t, []string{"/bin/sh", "-c", "top"}, cmd.Args)
require.Equal(t, syscall.SIGKILL, cmd.SysProcAttr.Pdeathsig)
// Missing home directory - HOME should still be set to the given
// home dir, but the command should set its CWD to root instead.
changeHomeDir(t, username, "/wrong/place")
usr.HomeDir = "/wrong/place"
root := string(os.PathSeparator)
expectedEnv[2] = "HOME=/wrong/place"
cmd, err = buildCommand(execCmd, usr, nil, nil)
require.NoError(t, err)
require.Equal(t, root, cmd.Dir)
require.Equal(t, expectedEnv, cmd.Env)
}
func TestConfigureCommand(t *testing.T) {
srv := newMockServer(t)
scx := newExecServerContext(t, srv)
unexpectedKey := "FOO"
unexpectedValue := "BAR"
// environment values in the server context should not be forwarded
scx.SetEnv(unexpectedKey, unexpectedValue)
cmd, err := ConfigureCommand(scx)
require.NoError(t, err)
require.NotNil(t, cmd)
require.Equal(t, "/proc/self/exe", cmd.Path)
require.NotContains(t, cmd.Env, unexpectedKey+"="+unexpectedValue)
}
// TestContinue tests if the process hangs if a continue signal is not sent
// and makes sure the process continues once it has been sent.
func TestContinue(t *testing.T) {
srv := newMockServer(t)
scx := newExecServerContext(t, srv)
// Configure Session Context to re-exec "ls".
var err error
lsPath, err := exec.LookPath("ls")
require.NoError(t, err)
scx.execRequest.SetCommand(lsPath)
// Create an exec.Cmd to execute through Teleport.
cmd, err := ConfigureCommand(scx)
require.NoError(t, err)
// Create a channel that will be used to signal that execution is complete.
cmdDone := make(chan error, 1)
// Re-execute Teleport and run "ls". Signal over the context when execution
// is complete.
go func() {
if err := cmd.Start(); err != nil {
cmdDone <- err
}
// Close the read half of the pipe to unblock the ready signal.
closeErr := scx.readyw.Close()
cmdDone <- trace.NewAggregate(closeErr, cmd.Wait())
}()
// Wait for the process. Since the continue pipe has not been closed, the
// process should not have exited yet.
select {
case err := <-cmdDone:
t.Fatalf("Process exited before continue with error %v", err)
case <-time.After(5 * time.Second):
}
// Wait for the child process to indicate its completed initialization.
require.NoError(t, scx.execRequest.WaitForChild())
// Signal to child that it may execute the requested program.
scx.execRequest.Continue()
// Program should have executed now. If the complete signal has not come
// over the context, something failed.
select {
case <-time.After(5 * time.Second):
t.Fatalf("Timed out waiting for process to finish.")
case err := <-cmdDone:
require.NoError(t, err)
}
}