Explicitly switch user for code execution.
Co-authored-by: Maximilian Pass <maximilian.pass@student.hpi.uni-potsdam.de>
This commit is contained in:

committed by
Sebastian Serth

parent
69237fb415
commit
1a5a49d7c8
2
Makefile
2
Makefile
@ -97,7 +97,7 @@ deploy/dockerfiles: ## Clone Dockerfiles repository
|
|||||||
@git clone git@github.com:$(REPOSITORY_OWNER)/dockerfiles.git deploy/dockerfiles
|
@git clone git@github.com:$(REPOSITORY_OWNER)/dockerfiles.git deploy/dockerfiles
|
||||||
|
|
||||||
.PHONY: e2e-test-docker-image
|
.PHONY: e2e-test-docker-image
|
||||||
e2e-test-docker-image: deploy/dockerfiles ## Build Docker image that is pushed to a registry and used in e2e tests
|
e2e-test-docker-image: deploy/dockerfiles ## Build Docker image that is used in e2e tests
|
||||||
@docker build -t $(E2E_TEST_DOCKER_IMAGE) deploy/dockerfiles/$(E2E_TEST_DOCKER_CONTAINER)/$(E2E_TEST_DOCKER_TAG)
|
@docker build -t $(E2E_TEST_DOCKER_IMAGE) deploy/dockerfiles/$(E2E_TEST_DOCKER_CONTAINER)/$(E2E_TEST_DOCKER_TAG)
|
||||||
|
|
||||||
.PHONY: e2e-test
|
.PHONY: e2e-test
|
||||||
|
@ -338,6 +338,10 @@ paths:
|
|||||||
description: The command to be executed. The working directory for this execution is the working directory of the image of the execution environment
|
description: The command to be executed. The working directory for this execution is the working directory of the image of the execution environment
|
||||||
type: string
|
type: string
|
||||||
example: python exercise.py
|
example: python exercise.py
|
||||||
|
privilegedExecution:
|
||||||
|
description: Specifies if the command should be executed as an privileged user.
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
environment:
|
environment:
|
||||||
description: Environment variables for this execution. The keys of this object are the variable names and the value of each key is the value of the variable with the same name. The environment variables of the system remain accessible.
|
description: Environment variables for this execution. The keys of this object are the variable names and the value of each key is the value of the variable with the same name. The environment variables of the system remain accessible.
|
||||||
type: object
|
type: object
|
||||||
|
@ -84,3 +84,14 @@ If the path is not set up correctly or with a different name, the placement of a
|
|||||||
### Use gVisor as a sandbox
|
### Use gVisor as a sandbox
|
||||||
|
|
||||||
We recommend using gVisor as a sandbox for the execution environments. First, [install gVisor following the official documentation](https://gvisor.dev/docs/user_guide/install/) and second, adapt the `/etc/docker/daemon.json` with reasonable defaults as shown in our [example configuration for Docker](./resources/docker.daemon.json).
|
We recommend using gVisor as a sandbox for the execution environments. First, [install gVisor following the official documentation](https://gvisor.dev/docs/user_guide/install/) and second, adapt the `/etc/docker/daemon.json` with reasonable defaults as shown in our [example configuration for Docker](./resources/docker.daemon.json).
|
||||||
|
|
||||||
|
## Supported Docker Images
|
||||||
|
|
||||||
|
In general, any Docker image can be used as an execution environment.
|
||||||
|
|
||||||
|
### Users
|
||||||
|
|
||||||
|
If the `privilegedExecution` flag is set to `true` during execution, no additional user is required. Otherwise, the following two requirements must be met:
|
||||||
|
|
||||||
|
- A non-privileged user called `user` needs to be present in the image. This user is used to execute the code.
|
||||||
|
- The Docker image needs to have a `/sbin/setuser` script allowing the execution of the user code as a non-root user, similar to `/usr/bin/su`.
|
||||||
|
@ -100,7 +100,8 @@ func (s *WebSocketTestSuite) TestWebsocketConnection() {
|
|||||||
s.Run("Executes the request in the runner", func() {
|
s.Run("Executes the request in the runner", func() {
|
||||||
<-time.After(tests.ShortTimeout)
|
<-time.After(tests.ShortTimeout)
|
||||||
s.apiMock.AssertCalled(s.T(), "ExecuteCommand",
|
s.apiMock.AssertCalled(s.T(), "ExecuteCommand",
|
||||||
mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything)
|
mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.AnythingOfType("bool"),
|
||||||
|
mock.Anything, mock.Anything, mock.Anything)
|
||||||
})
|
})
|
||||||
|
|
||||||
s.Run("Can send input", func() {
|
s.Run("Can send input", func() {
|
||||||
@ -405,6 +406,7 @@ var executionRequestSleep = dto.ExecutionRequest{Command: "sleep infinity"}
|
|||||||
// until the execution receives a SIGQUIT.
|
// until the execution receives a SIGQUIT.
|
||||||
func mockAPIExecuteSleep(api *nomad.ExecutorAPIMock) <-chan bool {
|
func mockAPIExecuteSleep(api *nomad.ExecutorAPIMock) <-chan bool {
|
||||||
canceled := make(chan bool, 1)
|
canceled := make(chan bool, 1)
|
||||||
|
|
||||||
mockAPIExecute(api, &executionRequestSleep,
|
mockAPIExecute(api, &executionRequestSleep,
|
||||||
func(_ string, ctx context.Context, _ []string, _ bool,
|
func(_ string, ctx context.Context, _ []string, _ bool,
|
||||||
stdin io.Reader, stdout io.Writer, stderr io.Writer,
|
stdin io.Reader, stdout io.Writer, stderr io.Writer,
|
||||||
@ -446,13 +448,13 @@ func mockAPIExecuteExitNonZero(api *nomad.ExecutorAPIMock) {
|
|||||||
// corresponding to the given ExecutionRequest is called.
|
// corresponding to the given ExecutionRequest is called.
|
||||||
func mockAPIExecute(api *nomad.ExecutorAPIMock, request *dto.ExecutionRequest,
|
func mockAPIExecute(api *nomad.ExecutorAPIMock, request *dto.ExecutionRequest,
|
||||||
run func(runnerId string, ctx context.Context, command []string, tty bool,
|
run func(runnerId string, ctx context.Context, command []string, tty bool,
|
||||||
stdin io.Reader, stdout, stderr io.Writer) (int, error),
|
stdin io.Reader, stdout, stderr io.Writer) (int, error)) {
|
||||||
) {
|
|
||||||
call := api.On("ExecuteCommand",
|
call := api.On("ExecuteCommand",
|
||||||
mock.AnythingOfType("string"),
|
mock.AnythingOfType("string"),
|
||||||
mock.Anything,
|
mock.Anything,
|
||||||
request.FullCommand(),
|
request.FullCommand(),
|
||||||
mock.AnythingOfType("bool"),
|
mock.AnythingOfType("bool"),
|
||||||
|
mock.AnythingOfType("bool"),
|
||||||
mock.Anything,
|
mock.Anything,
|
||||||
mock.Anything,
|
mock.Anything,
|
||||||
mock.Anything)
|
mock.Anything)
|
||||||
@ -461,9 +463,9 @@ func mockAPIExecute(api *nomad.ExecutorAPIMock, request *dto.ExecutionRequest,
|
|||||||
args.Get(1).(context.Context),
|
args.Get(1).(context.Context),
|
||||||
args.Get(2).([]string),
|
args.Get(2).([]string),
|
||||||
args.Get(3).(bool),
|
args.Get(3).(bool),
|
||||||
args.Get(4).(io.Reader),
|
args.Get(5).(io.Reader),
|
||||||
args.Get(5).(io.Writer),
|
args.Get(6).(io.Writer),
|
||||||
args.Get(6).(io.Writer))
|
args.Get(7).(io.Writer))
|
||||||
call.ReturnArguments = mock.Arguments{exit, err}
|
call.ReturnArguments = mock.Arguments{exit, err}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -78,20 +78,20 @@ func (_m *ExecutorAPIMock) Execute(jobID string, ctx context.Context, command []
|
|||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExecuteCommand provides a mock function with given fields: allocationID, ctx, command, tty, stdin, stdout, stderr
|
// ExecuteCommand provides a mock function with given fields: allocationID, ctx, command, tty, privilegedExecution, stdin, stdout, stderr
|
||||||
func (_m *ExecutorAPIMock) ExecuteCommand(allocationID string, ctx context.Context, command []string, tty bool, stdin io.Reader, stdout io.Writer, stderr io.Writer) (int, error) {
|
func (_m *ExecutorAPIMock) ExecuteCommand(allocationID string, ctx context.Context, command []string, tty bool, privilegedExecution bool, stdin io.Reader, stdout io.Writer, stderr io.Writer) (int, error) {
|
||||||
ret := _m.Called(allocationID, ctx, command, tty, stdin, stdout, stderr)
|
ret := _m.Called(allocationID, ctx, command, tty, privilegedExecution, stdin, stdout, stderr)
|
||||||
|
|
||||||
var r0 int
|
var r0 int
|
||||||
if rf, ok := ret.Get(0).(func(string, context.Context, []string, bool, io.Reader, io.Writer, io.Writer) int); ok {
|
if rf, ok := ret.Get(0).(func(string, context.Context, []string, bool, bool, io.Reader, io.Writer, io.Writer) int); ok {
|
||||||
r0 = rf(allocationID, ctx, command, tty, stdin, stdout, stderr)
|
r0 = rf(allocationID, ctx, command, tty, privilegedExecution, stdin, stdout, stderr)
|
||||||
} else {
|
} else {
|
||||||
r0 = ret.Get(0).(int)
|
r0 = ret.Get(0).(int)
|
||||||
}
|
}
|
||||||
|
|
||||||
var r1 error
|
var r1 error
|
||||||
if rf, ok := ret.Get(1).(func(string, context.Context, []string, bool, io.Reader, io.Writer, io.Writer) error); ok {
|
if rf, ok := ret.Get(1).(func(string, context.Context, []string, bool, bool, io.Reader, io.Writer, io.Writer) error); ok {
|
||||||
r1 = rf(allocationID, ctx, command, tty, stdin, stdout, stderr)
|
r1 = rf(allocationID, ctx, command, tty, privilegedExecution, stdin, stdout, stderr)
|
||||||
} else {
|
} else {
|
||||||
r1 = ret.Error(1)
|
r1 = ret.Error(1)
|
||||||
}
|
}
|
||||||
|
@ -70,7 +70,8 @@ type ExecutorAPI interface {
|
|||||||
// ExecuteCommand executes the given command in the allocation with the given id.
|
// ExecuteCommand executes the given command in the allocation with the given id.
|
||||||
// It writes the output of the command to stdout/stderr and reads input from stdin.
|
// It writes the output of the command to stdout/stderr and reads input from stdin.
|
||||||
// If tty is true, the command will run with a tty.
|
// If tty is true, the command will run with a tty.
|
||||||
ExecuteCommand(allocationID string, ctx context.Context, command []string, tty bool,
|
// Iff privilegedExecution is true, the command will be executed privileged.
|
||||||
|
ExecuteCommand(allocationID string, ctx context.Context, command []string, tty bool, privilegedExecution bool,
|
||||||
stdin io.Reader, stdout, stderr io.Writer) (int, error)
|
stdin io.Reader, stdout, stderr io.Writer) (int, error)
|
||||||
|
|
||||||
// MarkRunnerAsUsed marks the runner with the given ID as used. It also stores the timeout duration in the metadata.
|
// MarkRunnerAsUsed marks the runner with the given ID as used. It also stores the timeout duration in the metadata.
|
||||||
@ -390,12 +391,13 @@ func (a *APIClient) LoadEnvironmentJobs() ([]*nomadApi.Job, error) {
|
|||||||
// In order for the stderr splitting to work, the command must have the structure
|
// In order for the stderr splitting to work, the command must have the structure
|
||||||
// []string{..., "sh", "-c", "my-command"}.
|
// []string{..., "sh", "-c", "my-command"}.
|
||||||
func (a *APIClient) ExecuteCommand(allocationID string,
|
func (a *APIClient) ExecuteCommand(allocationID string,
|
||||||
ctx context.Context, command []string, tty bool,
|
ctx context.Context, command []string, tty bool, privilegedExecution bool,
|
||||||
stdin io.Reader, stdout, stderr io.Writer) (int, error) {
|
stdin io.Reader, stdout, stderr io.Writer) (int, error) {
|
||||||
if tty && config.Config.Server.InteractiveStderr {
|
if tty && config.Config.Server.InteractiveStderr {
|
||||||
return a.executeCommandInteractivelyWithStderr(allocationID, ctx, command, stdin, stdout, stderr)
|
return a.executeCommandInteractivelyWithStderr(allocationID, ctx, command, privilegedExecution, stdin, stdout, stderr)
|
||||||
}
|
}
|
||||||
exitCode, err := a.apiQuerier.Execute(allocationID, ctx, command, tty, stdin, stdout, stderr)
|
exitCode, err := a.apiQuerier.
|
||||||
|
Execute(allocationID, ctx, setUserCommand(command, privilegedExecution), tty, stdin, stdout, stderr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 1, fmt.Errorf("error executing command in API: %w", err)
|
return 1, fmt.Errorf("error executing command in API: %w", err)
|
||||||
}
|
}
|
||||||
@ -408,7 +410,7 @@ func (a *APIClient) ExecuteCommand(allocationID string,
|
|||||||
// to be served both over stdout. This function circumvents this by creating a fifo for the stderr
|
// to be served both over stdout. This function circumvents this by creating a fifo for the stderr
|
||||||
// of the command and starting a second execution that reads the stderr from that fifo.
|
// of the command and starting a second execution that reads the stderr from that fifo.
|
||||||
func (a *APIClient) executeCommandInteractivelyWithStderr(allocationID string, ctx context.Context,
|
func (a *APIClient) executeCommandInteractivelyWithStderr(allocationID string, ctx context.Context,
|
||||||
command []string, stdin io.Reader, stdout, stderr io.Writer) (int, error) {
|
command []string, privilegedExecution bool, stdin io.Reader, stdout, stderr io.Writer) (int, error) {
|
||||||
// Use current nano time to make the stderr fifo kind of unique.
|
// Use current nano time to make the stderr fifo kind of unique.
|
||||||
currentNanoTime := time.Now().UnixNano()
|
currentNanoTime := time.Now().UnixNano()
|
||||||
// We expect the command to be like []string{..., "sh", "-c", "my-command"}.
|
// We expect the command to be like []string{..., "sh", "-c", "my-command"}.
|
||||||
@ -422,7 +424,8 @@ func (a *APIClient) executeCommandInteractivelyWithStderr(allocationID string, c
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// Catch stderr in separate execution.
|
// Catch stderr in separate execution.
|
||||||
exit, err := a.Execute(allocationID, ctx, stderrFifoCommand(currentNanoTime), true,
|
stdErrCommand := setUserCommand(stderrFifoCommand(currentNanoTime), privilegedExecution)
|
||||||
|
exit, err := a.Execute(allocationID, ctx, stdErrCommand, true,
|
||||||
nullio.Reader{Ctx: readingContext}, stderr, io.Discard)
|
nullio.Reader{Ctx: readingContext}, stderr, io.Discard)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).WithField("runner", allocationID).Warn("Stderr task finished with error")
|
log.WithError(err).WithField("runner", allocationID).Warn("Stderr task finished with error")
|
||||||
@ -430,7 +433,8 @@ func (a *APIClient) executeCommandInteractivelyWithStderr(allocationID string, c
|
|||||||
stderrExitChan <- exit
|
stderrExitChan <- exit
|
||||||
}()
|
}()
|
||||||
|
|
||||||
exit, err := a.Execute(allocationID, ctx, command, true, stdin, stdout, io.Discard)
|
exit, err := a.
|
||||||
|
Execute(allocationID, ctx, setUserCommand(command, privilegedExecution), true, stdin, stdout, io.Discard)
|
||||||
|
|
||||||
// Wait until the stderr catch command finished to make sure we receive all output.
|
// Wait until the stderr catch command finished to make sure we receive all output.
|
||||||
<-stderrExitChan
|
<-stderrExitChan
|
||||||
@ -450,8 +454,25 @@ const (
|
|||||||
// redirected to the fifo.
|
// redirected to the fifo.
|
||||||
// Example: "until [ -e my.fifo ]; do sleep 0.01; done; (echo \"my.fifo exists\") 2> my.fifo".
|
// Example: "until [ -e my.fifo ]; do sleep 0.01; done; (echo \"my.fifo exists\") 2> my.fifo".
|
||||||
stderrWrapperCommandFormat = "until [ -e %s ]; do sleep 0.01; done; (%s) 2> %s"
|
stderrWrapperCommandFormat = "until [ -e %s ]; do sleep 0.01; done; (%s) 2> %s"
|
||||||
|
|
||||||
|
// setUserBinaryPath is due to Poseidon requires the setuser script for Nomad environments.
|
||||||
|
setUserBinaryPath = "/sbin/setuser"
|
||||||
|
// setUserBinaryUser is the user that is used and required by Poseidon for Nomad environments.
|
||||||
|
setUserBinaryUser = "user"
|
||||||
|
// PrivilegedExecution is to indicate the privileged execution of the passed command.
|
||||||
|
PrivilegedExecution = true
|
||||||
|
// UnprivilegedExecution is to indicate the unprivileged execution of the passed command.
|
||||||
|
UnprivilegedExecution = false
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func setUserCommand(command []string, privilegedExecution bool) []string {
|
||||||
|
if privilegedExecution {
|
||||||
|
return command
|
||||||
|
} else {
|
||||||
|
return append([]string{setUserBinaryPath, setUserBinaryUser}, command...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func stderrFifoCommand(id int64) []string {
|
func stderrFifoCommand(id int64) []string {
|
||||||
stderrFifoPath := stderrFifo(id)
|
stderrFifoPath := stderrFifo(id)
|
||||||
return []string{"sh", "-c", fmt.Sprintf(stderrFifoCommandFormat, stderrFifoPath, stderrFifoPath, stderrFifoPath)}
|
return []string{"sh", "-c", fmt.Sprintf(stderrFifoCommandFormat, stderrFifoPath, stderrFifoPath, stderrFifoPath)}
|
||||||
|
@ -676,28 +676,31 @@ func (s *ExecuteCommandTestSuite) TestWithSeparateStderr() {
|
|||||||
var calledStdoutCommand, calledStderrCommand []string
|
var calledStdoutCommand, calledStderrCommand []string
|
||||||
|
|
||||||
// mock regular call
|
// mock regular call
|
||||||
s.mockExecute(s.testCommandArray, commandExitCode, nil, func(args mock.Arguments) {
|
call := s.mockExecute(mock.AnythingOfType("[]string"), 0, nil, func(_ mock.Arguments) {})
|
||||||
|
call.Run(func(args mock.Arguments) {
|
||||||
var ok bool
|
var ok bool
|
||||||
calledStdoutCommand, ok = args.Get(2).([]string)
|
calledCommand, ok := args.Get(2).([]string)
|
||||||
s.Require().True(ok)
|
s.Require().True(ok)
|
||||||
writer, ok := args.Get(5).(io.Writer)
|
writer, ok := args.Get(5).(io.Writer)
|
||||||
s.Require().True(ok)
|
s.Require().True(ok)
|
||||||
_, err := writer.Write([]byte(s.expectedStdout))
|
|
||||||
|
s.Require().Equal(5, len(calledCommand))
|
||||||
|
isStderrCommand, err := regexp.MatchString("mkfifo.*", calledCommand[4])
|
||||||
s.Require().NoError(err)
|
s.Require().NoError(err)
|
||||||
})
|
if isStderrCommand {
|
||||||
// mock stderr call
|
calledStderrCommand = calledCommand
|
||||||
s.mockExecute(mock.AnythingOfType("[]string"), stderrExitCode, nil, func(args mock.Arguments) {
|
_, err = writer.Write([]byte(s.expectedStderr))
|
||||||
var ok bool
|
call.ReturnArguments = mock.Arguments{stderrExitCode, nil}
|
||||||
calledStderrCommand, ok = args.Get(2).([]string)
|
} else {
|
||||||
s.Require().True(ok)
|
calledStdoutCommand = calledCommand
|
||||||
writer, ok := args.Get(5).(io.Writer)
|
_, err = writer.Write([]byte(s.expectedStdout))
|
||||||
s.Require().True(ok)
|
call.ReturnArguments = mock.Arguments{commandExitCode, nil}
|
||||||
_, err := writer.Write([]byte(s.expectedStderr))
|
}
|
||||||
s.Require().NoError(err)
|
s.Require().NoError(err)
|
||||||
})
|
})
|
||||||
|
|
||||||
exitCode, err := s.nomadAPIClient.
|
exitCode, err := s.nomadAPIClient.ExecuteCommand(s.allocationID, s.ctx, s.testCommandArray, withTTY,
|
||||||
ExecuteCommand(s.allocationID, s.ctx, s.testCommandArray, withTTY, nullio.Reader{}, &stdout, &stderr)
|
UnprivilegedExecution, nullio.Reader{}, &stdout, &stderr)
|
||||||
s.Require().NoError(err)
|
s.Require().NoError(err)
|
||||||
|
|
||||||
s.apiMock.AssertNumberOfCalls(s.T(), "Execute", 2)
|
s.apiMock.AssertNumberOfCalls(s.T(), "Execute", 2)
|
||||||
@ -725,10 +728,26 @@ func (s *ExecuteCommandTestSuite) TestWithSeparateStderr() {
|
|||||||
|
|
||||||
func (s *ExecuteCommandTestSuite) TestWithSeparateStderrReturnsCommandError() {
|
func (s *ExecuteCommandTestSuite) TestWithSeparateStderrReturnsCommandError() {
|
||||||
config.Config.Server.InteractiveStderr = true
|
config.Config.Server.InteractiveStderr = true
|
||||||
s.mockExecute(s.testCommandArray, 1, tests.ErrDefault, func(args mock.Arguments) {})
|
|
||||||
s.mockExecute(mock.AnythingOfType("[]string"), 1, nil, func(args mock.Arguments) {})
|
call := s.mockExecute(mock.AnythingOfType("[]string"), 0, nil, func(_ mock.Arguments) {})
|
||||||
_, err := s.nomadAPIClient.
|
call.Run(func(args mock.Arguments) {
|
||||||
ExecuteCommand(s.allocationID, s.ctx, s.testCommandArray, withTTY, nullio.Reader{}, io.Discard, io.Discard)
|
var ok bool
|
||||||
|
calledCommand, ok := args.Get(2).([]string)
|
||||||
|
s.Require().True(ok)
|
||||||
|
|
||||||
|
s.Require().Equal(5, len(calledCommand))
|
||||||
|
isStderrCommand, err := regexp.MatchString("mkfifo.*", calledCommand[4])
|
||||||
|
s.Require().NoError(err)
|
||||||
|
if isStderrCommand {
|
||||||
|
call.ReturnArguments = mock.Arguments{1, nil}
|
||||||
|
} else {
|
||||||
|
call.ReturnArguments = mock.Arguments{1, tests.ErrDefault}
|
||||||
|
}
|
||||||
|
s.Require().NoError(err)
|
||||||
|
})
|
||||||
|
|
||||||
|
_, err := s.nomadAPIClient.ExecuteCommand(s.allocationID, s.ctx, s.testCommandArray, withTTY, UnprivilegedExecution,
|
||||||
|
nullio.Reader{}, io.Discard, io.Discard)
|
||||||
s.Equal(tests.ErrDefault, err)
|
s.Equal(tests.ErrDefault, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -738,7 +757,8 @@ func (s *ExecuteCommandTestSuite) TestWithoutSeparateStderr() {
|
|||||||
commandExitCode := 42
|
commandExitCode := 42
|
||||||
|
|
||||||
// mock regular call
|
// mock regular call
|
||||||
s.mockExecute(s.testCommandArray, commandExitCode, nil, func(args mock.Arguments) {
|
expectedCommand := setUserCommand(s.testCommandArray, UnprivilegedExecution)
|
||||||
|
s.mockExecute(expectedCommand, commandExitCode, nil, func(args mock.Arguments) {
|
||||||
stdout, ok := args.Get(5).(io.Writer)
|
stdout, ok := args.Get(5).(io.Writer)
|
||||||
s.Require().True(ok)
|
s.Require().True(ok)
|
||||||
_, err := stdout.Write([]byte(s.expectedStdout))
|
_, err := stdout.Write([]byte(s.expectedStdout))
|
||||||
@ -749,8 +769,8 @@ func (s *ExecuteCommandTestSuite) TestWithoutSeparateStderr() {
|
|||||||
s.Require().NoError(err)
|
s.Require().NoError(err)
|
||||||
})
|
})
|
||||||
|
|
||||||
exitCode, err := s.nomadAPIClient.
|
exitCode, err := s.nomadAPIClient.ExecuteCommand(s.allocationID, s.ctx, s.testCommandArray, withTTY,
|
||||||
ExecuteCommand(s.allocationID, s.ctx, s.testCommandArray, withTTY, nullio.Reader{}, &stdout, &stderr)
|
UnprivilegedExecution, nullio.Reader{}, &stdout, &stderr)
|
||||||
s.Require().NoError(err)
|
s.Require().NoError(err)
|
||||||
|
|
||||||
s.apiMock.AssertNumberOfCalls(s.T(), "Execute", 1)
|
s.apiMock.AssertNumberOfCalls(s.T(), "Execute", 1)
|
||||||
@ -761,15 +781,16 @@ func (s *ExecuteCommandTestSuite) TestWithoutSeparateStderr() {
|
|||||||
|
|
||||||
func (s *ExecuteCommandTestSuite) TestWithoutSeparateStderrReturnsCommandError() {
|
func (s *ExecuteCommandTestSuite) TestWithoutSeparateStderrReturnsCommandError() {
|
||||||
config.Config.Server.InteractiveStderr = false
|
config.Config.Server.InteractiveStderr = false
|
||||||
s.mockExecute(s.testCommandArray, 1, tests.ErrDefault, func(args mock.Arguments) {})
|
expectedCommand := setUserCommand(s.testCommandArray, UnprivilegedExecution)
|
||||||
_, err := s.nomadAPIClient.
|
s.mockExecute(expectedCommand, 1, tests.ErrDefault, func(args mock.Arguments) {})
|
||||||
ExecuteCommand(s.allocationID, s.ctx, s.testCommandArray, withTTY, nullio.Reader{}, io.Discard, io.Discard)
|
_, err := s.nomadAPIClient.ExecuteCommand(s.allocationID, s.ctx, s.testCommandArray, withTTY, UnprivilegedExecution,
|
||||||
|
nullio.Reader{}, io.Discard, io.Discard)
|
||||||
s.ErrorIs(err, tests.ErrDefault)
|
s.ErrorIs(err, tests.ErrDefault)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ExecuteCommandTestSuite) mockExecute(command interface{}, exitCode int,
|
func (s *ExecuteCommandTestSuite) mockExecute(command interface{}, exitCode int,
|
||||||
err error, runFunc func(arguments mock.Arguments)) {
|
err error, runFunc func(arguments mock.Arguments)) *mock.Call {
|
||||||
s.apiMock.On("Execute", s.allocationID, s.ctx, command, withTTY,
|
return s.apiMock.On("Execute", s.allocationID, s.ctx, command, withTTY,
|
||||||
mock.Anything, mock.Anything, mock.Anything).
|
mock.Anything, mock.Anything, mock.Anything).
|
||||||
Run(runFunc).
|
Run(runFunc).
|
||||||
Return(exitCode, err)
|
Return(exitCode, err)
|
||||||
|
@ -90,6 +90,7 @@ func (w *AWSFunctionWorkload) ExecuteInteractively(id string, _ io.ReadWriter, s
|
|||||||
return nil, nil, ErrorUnknownExecution
|
return nil, nil, ErrorUnknownExecution
|
||||||
}
|
}
|
||||||
hideEnvironmentVariables(request, "AWS")
|
hideEnvironmentVariables(request, "AWS")
|
||||||
|
request.PrivilegedExecution = true // AWS does not support multiple users at this moment.
|
||||||
command, ctx, cancel := prepareExecution(request)
|
command, ctx, cancel := prepareExecution(request)
|
||||||
exitInternal := make(chan ExitInfo)
|
exitInternal := make(chan ExitInfo)
|
||||||
exit := make(chan ExitInfo, 1)
|
exit := make(chan ExitInfo, 1)
|
||||||
|
@ -110,7 +110,7 @@ func (r *NomadJob) ExecuteInteractively(
|
|||||||
exit := make(chan ExitInfo, 1)
|
exit := make(chan ExitInfo, 1)
|
||||||
ctxExecute, cancelExecute := context.WithCancel(context.Background())
|
ctxExecute, cancelExecute := context.WithCancel(context.Background())
|
||||||
|
|
||||||
go r.executeCommand(ctxExecute, command, stdin, stdout, stderr, exitInternal)
|
go r.executeCommand(ctxExecute, command, request.PrivilegedExecution, stdin, stdout, stderr, exitInternal)
|
||||||
go r.handleExitOrContextDone(ctx, cancelExecute, exitInternal, exit, stdin)
|
go r.handleExitOrContextDone(ctx, cancelExecute, exitInternal, exit, stdin)
|
||||||
|
|
||||||
return exit, cancel, nil
|
return exit, cancel, nil
|
||||||
@ -130,6 +130,7 @@ func (r *NomadJob) UpdateFileSystem(copyRequest *dto.UpdateFileSystemRequest) er
|
|||||||
stdOut := bytes.Buffer{}
|
stdOut := bytes.Buffer{}
|
||||||
stdErr := bytes.Buffer{}
|
stdErr := bytes.Buffer{}
|
||||||
exitCode, err := r.api.ExecuteCommand(r.id, context.Background(), updateFileCommand, false,
|
exitCode, err := r.api.ExecuteCommand(r.id, context.Background(), updateFileCommand, false,
|
||||||
|
nomad.PrivilegedExecution, // All files should be written and owned by a privileged user #211.
|
||||||
&tarBuffer, &stdOut, &stdErr)
|
&tarBuffer, &stdOut, &stdErr)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -167,10 +168,10 @@ func prepareExecution(request *dto.ExecutionRequest) (
|
|||||||
return command, ctx, cancel
|
return command, ctx, cancel
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *NomadJob) executeCommand(ctx context.Context, command []string,
|
func (r *NomadJob) executeCommand(ctx context.Context, command []string, privilegedExecution bool,
|
||||||
stdin io.ReadWriter, stdout, stderr io.Writer, exit chan<- ExitInfo,
|
stdin io.ReadWriter, stdout, stderr io.Writer, exit chan<- ExitInfo,
|
||||||
) {
|
) {
|
||||||
exitCode, err := r.api.ExecuteCommand(r.id, ctx, command, true, stdin, stdout, stderr)
|
exitCode, err := r.api.ExecuteCommand(r.id, ctx, command, true, privilegedExecution, stdin, stdout, stderr)
|
||||||
if err == nil && r.TimeoutPassed() {
|
if err == nil && r.TimeoutPassed() {
|
||||||
err = ErrorRunnerInactivityTimeout
|
err = ErrorRunnerInactivityTimeout
|
||||||
}
|
}
|
||||||
|
@ -111,8 +111,8 @@ type ExecuteInteractivelyTestSuite struct {
|
|||||||
|
|
||||||
func (s *ExecuteInteractivelyTestSuite) SetupTest() {
|
func (s *ExecuteInteractivelyTestSuite) SetupTest() {
|
||||||
s.apiMock = &nomad.ExecutorAPIMock{}
|
s.apiMock = &nomad.ExecutorAPIMock{}
|
||||||
s.mockedExecuteCommandCall = s.apiMock.
|
s.mockedExecuteCommandCall = s.apiMock.On("ExecuteCommand", mock.Anything, mock.Anything, mock.Anything,
|
||||||
On("ExecuteCommand", mock.Anything, mock.Anything, mock.Anything, true, mock.Anything, mock.Anything, mock.Anything).
|
true, false, mock.Anything, mock.Anything, mock.Anything).
|
||||||
Return(0, nil)
|
Return(0, nil)
|
||||||
s.timer = &InactivityTimerMock{}
|
s.timer = &InactivityTimerMock{}
|
||||||
s.timer.On("ResetTimeout").Return()
|
s.timer.On("ResetTimeout").Return()
|
||||||
@ -142,7 +142,7 @@ func (s *ExecuteInteractivelyTestSuite) TestCallsApi() {
|
|||||||
|
|
||||||
time.Sleep(tests.ShortTimeout)
|
time.Sleep(tests.ShortTimeout)
|
||||||
s.apiMock.AssertCalled(s.T(), "ExecuteCommand", tests.DefaultRunnerID, mock.Anything, request.FullCommand(),
|
s.apiMock.AssertCalled(s.T(), "ExecuteCommand", tests.DefaultRunnerID, mock.Anything, request.FullCommand(),
|
||||||
true, mock.Anything, mock.Anything, mock.Anything)
|
true, false, mock.Anything, mock.Anything, mock.Anything)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ExecuteInteractivelyTestSuite) TestReturnsAfterTimeout() {
|
func (s *ExecuteInteractivelyTestSuite) TestReturnsAfterTimeout() {
|
||||||
@ -173,7 +173,7 @@ func (s *ExecuteInteractivelyTestSuite) TestReturnsAfterTimeout() {
|
|||||||
func (s *ExecuteInteractivelyTestSuite) TestSendsSignalAfterTimeout() {
|
func (s *ExecuteInteractivelyTestSuite) TestSendsSignalAfterTimeout() {
|
||||||
quit := make(chan struct{})
|
quit := make(chan struct{})
|
||||||
s.mockedExecuteCommandCall.Run(func(args mock.Arguments) {
|
s.mockedExecuteCommandCall.Run(func(args mock.Arguments) {
|
||||||
stdin, ok := args.Get(4).(io.Reader)
|
stdin, ok := args.Get(5).(io.Reader)
|
||||||
s.Require().True(ok)
|
s.Require().True(ok)
|
||||||
buffer := make([]byte, 1) //nolint:makezero,lll // If the length is zero, the Read call never reads anything. gofmt want this alignment.
|
buffer := make([]byte, 1) //nolint:makezero,lll // If the length is zero, the Read call never reads anything. gofmt want this alignment.
|
||||||
for n := 0; !(n == 1 && buffer[0] == SIGQUIT); {
|
for n := 0; !(n == 1 && buffer[0] == SIGQUIT); {
|
||||||
@ -257,12 +257,12 @@ func (s *UpdateFileSystemTestSuite) SetupTest() {
|
|||||||
api: s.apiMock,
|
api: s.apiMock,
|
||||||
}
|
}
|
||||||
s.mockedExecuteCommandCall = s.apiMock.On("ExecuteCommand", tests.DefaultRunnerID, mock.Anything,
|
s.mockedExecuteCommandCall = s.apiMock.On("ExecuteCommand", tests.DefaultRunnerID, mock.Anything,
|
||||||
mock.Anything, false, mock.Anything, mock.Anything, mock.Anything).
|
mock.Anything, false, mock.AnythingOfType("bool"), mock.Anything, mock.Anything, mock.Anything).
|
||||||
Run(func(args mock.Arguments) {
|
Run(func(args mock.Arguments) {
|
||||||
var ok bool
|
var ok bool
|
||||||
s.command, ok = args.Get(2).([]string)
|
s.command, ok = args.Get(2).([]string)
|
||||||
s.Require().True(ok)
|
s.Require().True(ok)
|
||||||
s.stdin, ok = args.Get(4).(*bytes.Buffer)
|
s.stdin, ok = args.Get(5).(*bytes.Buffer)
|
||||||
s.Require().True(ok)
|
s.Require().True(ok)
|
||||||
}).Return(0, nil)
|
}).Return(0, nil)
|
||||||
}
|
}
|
||||||
@ -274,7 +274,7 @@ func (s *UpdateFileSystemTestSuite) TestUpdateFileSystemForRunnerPerformsTarExtr
|
|||||||
err := s.runner.UpdateFileSystem(copyRequest)
|
err := s.runner.UpdateFileSystem(copyRequest)
|
||||||
s.NoError(err)
|
s.NoError(err)
|
||||||
s.apiMock.AssertCalled(s.T(), "ExecuteCommand", mock.Anything, mock.Anything, mock.Anything,
|
s.apiMock.AssertCalled(s.T(), "ExecuteCommand", mock.Anything, mock.Anything, mock.Anything,
|
||||||
false, mock.Anything, mock.Anything, mock.Anything)
|
false, mock.AnythingOfType("bool"), mock.Anything, mock.Anything, mock.Anything)
|
||||||
s.Regexp("tar --extract --absolute-names", s.command)
|
s.Regexp("tar --extract --absolute-names", s.command)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -297,7 +297,7 @@ func (s *UpdateFileSystemTestSuite) TestFilesToCopyAreIncludedInTarArchive() {
|
|||||||
{Path: tests.DefaultFileName, Content: []byte(tests.DefaultFileContent)}}}
|
{Path: tests.DefaultFileName, Content: []byte(tests.DefaultFileContent)}}}
|
||||||
err := s.runner.UpdateFileSystem(copyRequest)
|
err := s.runner.UpdateFileSystem(copyRequest)
|
||||||
s.NoError(err)
|
s.NoError(err)
|
||||||
s.apiMock.AssertCalled(s.T(), "ExecuteCommand", mock.Anything, mock.Anything, mock.Anything, false,
|
s.apiMock.AssertCalled(s.T(), "ExecuteCommand", mock.Anything, mock.Anything, mock.Anything, false, true,
|
||||||
mock.Anything, mock.Anything, mock.Anything)
|
mock.Anything, mock.Anything, mock.Anything)
|
||||||
|
|
||||||
tarFiles := s.readFilesFromTarArchive(s.stdin)
|
tarFiles := s.readFilesFromTarArchive(s.stdin)
|
||||||
@ -348,7 +348,7 @@ func (s *UpdateFileSystemTestSuite) TestFilesToRemoveGetRemoved() {
|
|||||||
copyRequest := &dto.UpdateFileSystemRequest{Delete: []dto.FilePath{tests.DefaultFileName}}
|
copyRequest := &dto.UpdateFileSystemRequest{Delete: []dto.FilePath{tests.DefaultFileName}}
|
||||||
err := s.runner.UpdateFileSystem(copyRequest)
|
err := s.runner.UpdateFileSystem(copyRequest)
|
||||||
s.NoError(err)
|
s.NoError(err)
|
||||||
s.apiMock.AssertCalled(s.T(), "ExecuteCommand", mock.Anything, mock.Anything, mock.Anything, false,
|
s.apiMock.AssertCalled(s.T(), "ExecuteCommand", mock.Anything, mock.Anything, mock.Anything, false, true,
|
||||||
mock.Anything, mock.Anything, mock.Anything)
|
mock.Anything, mock.Anything, mock.Anything)
|
||||||
s.Regexp(fmt.Sprintf("rm[^;]+%s' *;", regexp.QuoteMeta(tests.DefaultFileName)), s.command)
|
s.Regexp(fmt.Sprintf("rm[^;]+%s' *;", regexp.QuoteMeta(tests.DefaultFileName)), s.command)
|
||||||
}
|
}
|
||||||
@ -357,7 +357,7 @@ func (s *UpdateFileSystemTestSuite) TestFilesToRemoveGetEscaped() {
|
|||||||
copyRequest := &dto.UpdateFileSystemRequest{Delete: []dto.FilePath{"/some/potentially/harmful'filename"}}
|
copyRequest := &dto.UpdateFileSystemRequest{Delete: []dto.FilePath{"/some/potentially/harmful'filename"}}
|
||||||
err := s.runner.UpdateFileSystem(copyRequest)
|
err := s.runner.UpdateFileSystem(copyRequest)
|
||||||
s.NoError(err)
|
s.NoError(err)
|
||||||
s.apiMock.AssertCalled(s.T(), "ExecuteCommand", mock.Anything, mock.Anything, mock.Anything, false,
|
s.apiMock.AssertCalled(s.T(), "ExecuteCommand", mock.Anything, mock.Anything, mock.Anything, false, true,
|
||||||
mock.Anything, mock.Anything, mock.Anything)
|
mock.Anything, mock.Anything, mock.Anything)
|
||||||
s.Contains(strings.Join(s.command, " "), "'/some/potentially/harmful'\\''filename'")
|
s.Contains(strings.Join(s.command, " "), "'/some/potentially/harmful'\\''filename'")
|
||||||
}
|
}
|
||||||
|
@ -18,10 +18,13 @@ type RunnerRequest struct {
|
|||||||
// ExecutionRequest is the expected json structure of the request body for the ExecuteCommand function.
|
// ExecutionRequest is the expected json structure of the request body for the ExecuteCommand function.
|
||||||
type ExecutionRequest struct {
|
type ExecutionRequest struct {
|
||||||
Command string
|
Command string
|
||||||
|
PrivilegedExecution bool
|
||||||
TimeLimit int
|
TimeLimit int
|
||||||
Environment map[string]string
|
Environment map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FullCommand joins the environment variables and the passed command into an "sh -c" wrapped command.
|
||||||
|
// It does not handle the TimeLimit or the PrivilegedExecution flag.
|
||||||
func (er *ExecutionRequest) FullCommand() []string {
|
func (er *ExecutionRequest) FullCommand() []string {
|
||||||
command := make([]string, 0)
|
command := make([]string, 0)
|
||||||
command = append(command, "env")
|
command = append(command, "env")
|
||||||
|
@ -224,7 +224,7 @@ func (s *E2ETestSuite) TestCopyFilesRoute_PermissionDenied() {
|
|||||||
newFileContent := []byte("New content")
|
newFileContent := []byte("New content")
|
||||||
copyFilesRequestByteString, err := json.Marshal(&dto.UpdateFileSystemRequest{
|
copyFilesRequestByteString, err := json.Marshal(&dto.UpdateFileSystemRequest{
|
||||||
Copy: []dto.File{
|
Copy: []dto.File{
|
||||||
{Path: "/dev/sda", Content: []byte(tests.DefaultFileContent)},
|
{Path: "/proc/1/environ", Content: []byte(tests.DefaultFileContent)},
|
||||||
{Path: tests.DefaultFileName, Content: newFileContent},
|
{Path: tests.DefaultFileName, Content: newFileContent},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -237,7 +237,7 @@ func (s *E2ETestSuite) TestCopyFilesRoute_PermissionDenied() {
|
|||||||
internalServerError := new(dto.InternalServerError)
|
internalServerError := new(dto.InternalServerError)
|
||||||
err = json.NewDecoder(resp.Body).Decode(internalServerError)
|
err = json.NewDecoder(resp.Body).Decode(internalServerError)
|
||||||
s.NoError(err)
|
s.NoError(err)
|
||||||
s.Contains(internalServerError.Message, "Cannot open: Permission denied")
|
s.Contains(internalServerError.Message, "Cannot open: ")
|
||||||
_ = resp.Body.Close()
|
_ = resp.Body.Close()
|
||||||
|
|
||||||
s.Run("File content can be printed on runner", func() {
|
s.Run("File content can be printed on runner", func() {
|
||||||
@ -257,7 +257,7 @@ func (s *E2ETestSuite) TestCopyFilesRoute_PermissionDenied() {
|
|||||||
newFileContent := []byte("New content")
|
newFileContent := []byte("New content")
|
||||||
copyFilesRequestByteString, err := json.Marshal(&dto.UpdateFileSystemRequest{
|
copyFilesRequestByteString, err := json.Marshal(&dto.UpdateFileSystemRequest{
|
||||||
Copy: []dto.File{
|
Copy: []dto.File{
|
||||||
{Path: "/dev/sda", Content: []byte(tests.DefaultFileContent)},
|
{Path: "/proc/1/environ", Content: []byte(tests.DefaultFileContent)},
|
||||||
{Path: tests.DefaultFileName, Content: newFileContent},
|
{Path: tests.DefaultFileName, Content: newFileContent},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -271,7 +271,7 @@ func (s *E2ETestSuite) TestCopyFilesRoute_PermissionDenied() {
|
|||||||
|
|
||||||
stdout, stderr := s.PrintContentOfFileOnRunner(runnerID, tests.DefaultFileName)
|
stdout, stderr := s.PrintContentOfFileOnRunner(runnerID, tests.DefaultFileName)
|
||||||
s.Equal(string(newFileContent), stdout)
|
s.Equal(string(newFileContent), stdout)
|
||||||
s.Contains(stderr, "Permission denied")
|
s.Contains(stderr, "Exception")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -82,6 +82,19 @@ func (s *E2ETestSuite) TestOutputToStderr() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *E2ETestSuite) TestUserNomad() {
|
||||||
|
s.Run("unprivileged", func() {
|
||||||
|
stdout, _, _ := ExecuteNonInteractive(&s.Suite, tests.DefaultEnvironmentIDAsInteger,
|
||||||
|
&dto.ExecutionRequest{Command: "id --name --user", PrivilegedExecution: false}, nil)
|
||||||
|
s.Require().NotEqual("root", stdout)
|
||||||
|
})
|
||||||
|
s.Run("privileged", func() {
|
||||||
|
stdout, _, _ := ExecuteNonInteractive(&s.Suite, tests.DefaultEnvironmentIDAsInteger,
|
||||||
|
&dto.ExecutionRequest{Command: "id --name --user", PrivilegedExecution: true}, nil)
|
||||||
|
s.Require().Equal("root\r\n", stdout)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// AWS environments do not support stdin at this moment therefore they cannot take this test.
|
// AWS environments do not support stdin at this moment therefore they cannot take this test.
|
||||||
func (s *E2ETestSuite) TestCommandHead() {
|
func (s *E2ETestSuite) TestCommandHead() {
|
||||||
hello := "Hello World!"
|
hello := "Hello World!"
|
||||||
|
Reference in New Issue
Block a user