Explicitly switch user for code execution.

Co-authored-by: Maximilian Pass <maximilian.pass@student.hpi.uni-potsdam.de>
This commit is contained in:
Sebastian Serth
2022-09-18 01:52:15 +02:00
committed by Sebastian Serth
parent 69237fb415
commit 1a5a49d7c8
13 changed files with 144 additions and 67 deletions

View File

@@ -78,20 +78,20 @@ func (_m *ExecutorAPIMock) Execute(jobID string, ctx context.Context, command []
return r0, r1
}
// ExecuteCommand provides a mock function with given fields: allocationID, ctx, command, tty, 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) {
ret := _m.Called(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, privilegedExecution bool, stdin io.Reader, stdout io.Writer, stderr io.Writer) (int, error) {
ret := _m.Called(allocationID, ctx, command, tty, privilegedExecution, stdin, stdout, stderr)
var r0 int
if rf, ok := ret.Get(0).(func(string, context.Context, []string, bool, io.Reader, io.Writer, io.Writer) int); ok {
r0 = rf(allocationID, ctx, command, tty, stdin, stdout, stderr)
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, privilegedExecution, stdin, stdout, stderr)
} else {
r0 = ret.Get(0).(int)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, context.Context, []string, bool, io.Reader, io.Writer, io.Writer) error); ok {
r1 = rf(allocationID, ctx, command, tty, stdin, stdout, stderr)
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, privilegedExecution, stdin, stdout, stderr)
} else {
r1 = ret.Error(1)
}

View File

@@ -70,7 +70,8 @@ type ExecutorAPI interface {
// 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.
// 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)
// 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
// []string{..., "sh", "-c", "my-command"}.
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) {
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 {
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
// of the command and starting a second execution that reads the stderr from that fifo.
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.
currentNanoTime := time.Now().UnixNano()
// 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()
// 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)
if err != nil {
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
}()
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.
<-stderrExitChan
@@ -450,8 +454,25 @@ const (
// redirected to the 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"
// 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 {
stderrFifoPath := stderrFifo(id)
return []string{"sh", "-c", fmt.Sprintf(stderrFifoCommandFormat, stderrFifoPath, stderrFifoPath, stderrFifoPath)}

View File

@@ -676,28 +676,31 @@ func (s *ExecuteCommandTestSuite) TestWithSeparateStderr() {
var calledStdoutCommand, calledStderrCommand []string
// 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
calledStdoutCommand, ok = args.Get(2).([]string)
calledCommand, ok := args.Get(2).([]string)
s.Require().True(ok)
writer, ok := args.Get(5).(io.Writer)
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)
})
// mock stderr call
s.mockExecute(mock.AnythingOfType("[]string"), stderrExitCode, nil, func(args mock.Arguments) {
var ok bool
calledStderrCommand, ok = args.Get(2).([]string)
s.Require().True(ok)
writer, ok := args.Get(5).(io.Writer)
s.Require().True(ok)
_, err := writer.Write([]byte(s.expectedStderr))
if isStderrCommand {
calledStderrCommand = calledCommand
_, err = writer.Write([]byte(s.expectedStderr))
call.ReturnArguments = mock.Arguments{stderrExitCode, nil}
} else {
calledStdoutCommand = calledCommand
_, err = writer.Write([]byte(s.expectedStdout))
call.ReturnArguments = mock.Arguments{commandExitCode, nil}
}
s.Require().NoError(err)
})
exitCode, err := s.nomadAPIClient.
ExecuteCommand(s.allocationID, s.ctx, s.testCommandArray, withTTY, nullio.Reader{}, &stdout, &stderr)
exitCode, err := s.nomadAPIClient.ExecuteCommand(s.allocationID, s.ctx, s.testCommandArray, withTTY,
UnprivilegedExecution, nullio.Reader{}, &stdout, &stderr)
s.Require().NoError(err)
s.apiMock.AssertNumberOfCalls(s.T(), "Execute", 2)
@@ -725,10 +728,26 @@ func (s *ExecuteCommandTestSuite) TestWithSeparateStderr() {
func (s *ExecuteCommandTestSuite) TestWithSeparateStderrReturnsCommandError() {
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) {})
_, err := s.nomadAPIClient.
ExecuteCommand(s.allocationID, s.ctx, s.testCommandArray, withTTY, nullio.Reader{}, io.Discard, io.Discard)
call := s.mockExecute(mock.AnythingOfType("[]string"), 0, nil, func(_ mock.Arguments) {})
call.Run(func(args mock.Arguments) {
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)
}
@@ -738,7 +757,8 @@ func (s *ExecuteCommandTestSuite) TestWithoutSeparateStderr() {
commandExitCode := 42
// 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)
s.Require().True(ok)
_, err := stdout.Write([]byte(s.expectedStdout))
@@ -749,8 +769,8 @@ func (s *ExecuteCommandTestSuite) TestWithoutSeparateStderr() {
s.Require().NoError(err)
})
exitCode, err := s.nomadAPIClient.
ExecuteCommand(s.allocationID, s.ctx, s.testCommandArray, withTTY, nullio.Reader{}, &stdout, &stderr)
exitCode, err := s.nomadAPIClient.ExecuteCommand(s.allocationID, s.ctx, s.testCommandArray, withTTY,
UnprivilegedExecution, nullio.Reader{}, &stdout, &stderr)
s.Require().NoError(err)
s.apiMock.AssertNumberOfCalls(s.T(), "Execute", 1)
@@ -761,15 +781,16 @@ func (s *ExecuteCommandTestSuite) TestWithoutSeparateStderr() {
func (s *ExecuteCommandTestSuite) TestWithoutSeparateStderrReturnsCommandError() {
config.Config.Server.InteractiveStderr = false
s.mockExecute(s.testCommandArray, 1, tests.ErrDefault, func(args mock.Arguments) {})
_, err := s.nomadAPIClient.
ExecuteCommand(s.allocationID, s.ctx, s.testCommandArray, withTTY, nullio.Reader{}, io.Discard, io.Discard)
expectedCommand := setUserCommand(s.testCommandArray, UnprivilegedExecution)
s.mockExecute(expectedCommand, 1, tests.ErrDefault, func(args mock.Arguments) {})
_, err := s.nomadAPIClient.ExecuteCommand(s.allocationID, s.ctx, s.testCommandArray, withTTY, UnprivilegedExecution,
nullio.Reader{}, io.Discard, io.Discard)
s.ErrorIs(err, tests.ErrDefault)
}
func (s *ExecuteCommandTestSuite) mockExecute(command interface{}, exitCode int,
err error, runFunc func(arguments mock.Arguments)) {
s.apiMock.On("Execute", s.allocationID, s.ctx, command, withTTY,
err error, runFunc func(arguments mock.Arguments)) *mock.Call {
return s.apiMock.On("Execute", s.allocationID, s.ctx, command, withTTY,
mock.Anything, mock.Anything, mock.Anything).
Run(runFunc).
Return(exitCode, err)