Enable unprivileged retrieve of file listing and content.

This commit is contained in:
Maximilian Paß
2022-09-28 18:50:27 +01:00
parent 0218b3589a
commit 0c70ad3b24
8 changed files with 49 additions and 25 deletions

View File

@ -330,6 +330,12 @@ paths:
format: pct-encoded # rfc 3986 format: pct-encoded # rfc 3986
default: "./" default: "./"
required: false required: false
- name: privilegedExecution
in: query
description: Specifies if the command should be executed as an privileged user.
schema:
type: boolean
default: false
responses: responses:
"200": "200":
description: Success. Returns the listing of the runner's filesystem. description: Success. Returns the listing of the runner's filesystem.
@ -423,6 +429,12 @@ paths:
format: pct-encoded # rfc 3986 format: pct-encoded # rfc 3986
example: "./flag.txt" example: "./flag.txt"
required: true required: true
- name: privilegedExecution
in: query
description: Specifies if the command should be executed as an privileged user.
schema:
type: boolean
default: false
responses: responses:
"200": "200":
description: Success. Returns the file. description: Success. Returns the file.

View File

@ -28,6 +28,7 @@ const (
ExecutionIDKey = "executionID" ExecutionIDKey = "executionID"
PathKey = "path" PathKey = "path"
RecursiveKey = "recursive" RecursiveKey = "recursive"
PrivilegedExecutionKey = "privilegedExecution"
) )
type RunnerController struct { type RunnerController struct {
@ -93,9 +94,13 @@ func (r *RunnerController) listFileSystem(writer http.ResponseWriter, request *h
if path == "" { if path == "" {
path = "./" path = "./"
} }
privilegedExecution, err := strconv.ParseBool(request.URL.Query().Get(PrivilegedExecutionKey))
if err != nil {
privilegedExecution = false
}
writer.Header().Set("Content-Type", "application/json") writer.Header().Set("Content-Type", "application/json")
err = targetRunner.ListFileSystem(path, recursive, writer, request.Context()) err = targetRunner.ListFileSystem(path, recursive, writer, privilegedExecution, request.Context())
if errors.Is(err, runner.ErrFileNotFound) { if errors.Is(err, runner.ErrFileNotFound) {
writeClientError(writer, err, http.StatusFailedDependency) writeClientError(writer, err, http.StatusFailedDependency)
return return
@ -129,9 +134,13 @@ func (r *RunnerController) updateFileSystem(writer http.ResponseWriter, request
func (r *RunnerController) fileContent(writer http.ResponseWriter, request *http.Request) { func (r *RunnerController) fileContent(writer http.ResponseWriter, request *http.Request) {
targetRunner, _ := runner.FromContext(request.Context()) targetRunner, _ := runner.FromContext(request.Context())
path := request.URL.Query().Get(PathKey) path := request.URL.Query().Get(PathKey)
privilegedExecution, err := strconv.ParseBool(request.URL.Query().Get(PrivilegedExecutionKey))
if err != nil {
privilegedExecution = false
}
writer.Header().Set("Content-Type", "application/octet-stream") writer.Header().Set("Content-Type", "application/octet-stream")
err := targetRunner.GetFileContent(path, writer, request.Context()) err = targetRunner.GetFileContent(path, writer, privilegedExecution, request.Context())
if errors.Is(err, runner.ErrFileNotFound) { if errors.Is(err, runner.ErrFileNotFound) {
writeClientError(writer, err, http.StatusFailedDependency) writeClientError(writer, err, http.StatusFailedDependency)
return return

View File

@ -315,8 +315,8 @@ func (s *UpdateFileSystemRouteTestSuite) TestUpdateFileSystemReturnsInternalServ
func (s *UpdateFileSystemRouteTestSuite) TestListFileSystem() { func (s *UpdateFileSystemRouteTestSuite) TestListFileSystem() {
routeURL, err := s.router.Get(UpdateFileSystemPath).URL(RunnerIDKey, tests.DefaultMockID) routeURL, err := s.router.Get(UpdateFileSystemPath).URL(RunnerIDKey, tests.DefaultMockID)
s.Require().NoError(err) s.Require().NoError(err)
mockCall := s.runnerMock.On("ListFileSystem", mockCall := s.runnerMock.On("ListFileSystem", mock.AnythingOfType("string"),
mock.AnythingOfType("string"), mock.AnythingOfType("bool"), mock.Anything, mock.Anything) mock.AnythingOfType("bool"), mock.Anything, mock.AnythingOfType("bool"), mock.Anything)
s.Run("default parameters", func() { s.Run("default parameters", func() {
mockCall.Run(func(args mock.Arguments) { mockCall.Run(func(args mock.Arguments) {
@ -375,8 +375,8 @@ func (s *UpdateFileSystemRouteTestSuite) TestListFileSystem() {
func (s *UpdateFileSystemRouteTestSuite) TestFileContent() { func (s *UpdateFileSystemRouteTestSuite) TestFileContent() {
routeURL, err := s.router.Get(FileContentRawPath).URL(RunnerIDKey, tests.DefaultMockID) routeURL, err := s.router.Get(FileContentRawPath).URL(RunnerIDKey, tests.DefaultMockID)
s.Require().NoError(err) s.Require().NoError(err)
mockCall := s.runnerMock. mockCall := s.runnerMock.On("GetFileContent",
On("GetFileContent", mock.AnythingOfType("string"), mock.Anything, mock.Anything) mock.AnythingOfType("string"), mock.Anything, mock.AnythingOfType("bool"), mock.Anything)
s.Run("Not Found", func() { s.Run("Not Found", func() {
mockCall.Return(runner.ErrFileNotFound) mockCall.Return(runner.ErrFileNotFound)

View File

@ -104,7 +104,7 @@ func (w *AWSFunctionWorkload) ExecuteInteractively(id string, _ io.ReadWriter, s
// ListFileSystem is currently not supported with this aws serverless function. // ListFileSystem is currently not supported with this aws serverless function.
// This is because the function execution ends with the termination of the workload code. // This is because the function execution ends with the termination of the workload code.
// So an on-demand file system listing after the termination is not possible. Also, we do not want to copy all files. // So an on-demand file system listing after the termination is not possible. Also, we do not want to copy all files.
func (w *AWSFunctionWorkload) ListFileSystem(_ string, _ bool, _ io.Writer, _ context.Context) error { func (w *AWSFunctionWorkload) ListFileSystem(_ string, _ bool, _ io.Writer, _ bool, _ context.Context) error {
return dto.ErrNotSupported return dto.ErrNotSupported
} }
@ -125,7 +125,7 @@ func (w *AWSFunctionWorkload) UpdateFileSystem(request *dto.UpdateFileSystemRequ
// GetFileContent is currently not supported with this aws serverless function. // GetFileContent is currently not supported with this aws serverless function.
// This is because the function execution ends with the termination of the workload code. // This is because the function execution ends with the termination of the workload code.
// So an on-demand file streaming after the termination is not possible. Also, we do not want to copy all files. // So an on-demand file streaming after the termination is not possible. Also, we do not want to copy all files.
func (w *AWSFunctionWorkload) GetFileContent(_ string, _ io.Writer, _ context.Context) error { func (w *AWSFunctionWorkload) GetFileContent(_ string, _ io.Writer, _ bool, _ context.Context) error {
return dto.ErrNotSupported return dto.ErrNotSupported
} }

View File

@ -118,7 +118,8 @@ func (r *NomadJob) ExecuteInteractively(
return exit, cancel, nil return exit, cancel, nil
} }
func (r *NomadJob) ListFileSystem(path string, recursive bool, content io.Writer, ctx context.Context) error { func (r *NomadJob) ListFileSystem(
path string, recursive bool, content io.Writer, privilegedExecution bool, ctx context.Context) error {
r.ResetTimeout() r.ResetTimeout()
command := "ls -l --time-style=+%s -1 --literal" command := "ls -l --time-style=+%s -1 --literal"
if recursive { if recursive {
@ -128,7 +129,8 @@ func (r *NomadJob) ListFileSystem(path string, recursive bool, content io.Writer
ls2json := &nullio.Ls2JsonWriter{Target: content} ls2json := &nullio.Ls2JsonWriter{Target: content}
defer ls2json.Close() defer ls2json.Close()
retrieveCommand := (&dto.ExecutionRequest{Command: fmt.Sprintf("%s %q", command, path)}).FullCommand() retrieveCommand := (&dto.ExecutionRequest{Command: fmt.Sprintf("%s %q", command, path)}).FullCommand()
exitCode, err := r.api.ExecuteCommand(r.id, ctx, retrieveCommand, false, &nullio.Reader{}, ls2json, io.Discard) exitCode, err := r.api.ExecuteCommand(r.id, ctx, retrieveCommand, false, privilegedExecution,
&nullio.Reader{}, ls2json, io.Discard)
if err != nil { if err != nil {
return fmt.Errorf("%w: nomad error during retrieve file headers: %v", return fmt.Errorf("%w: nomad error during retrieve file headers: %v",
nomad.ErrorExecutorCommunicationFailed, err) nomad.ErrorExecutorCommunicationFailed, err)
@ -172,12 +174,13 @@ func (r *NomadJob) UpdateFileSystem(copyRequest *dto.UpdateFileSystemRequest) er
return nil return nil
} }
func (r *NomadJob) GetFileContent(path string, content io.Writer, ctx context.Context) error { func (r *NomadJob) GetFileContent(path string, content io.Writer, privilegedExecution bool, ctx context.Context) error {
r.ResetTimeout() r.ResetTimeout()
retrieveCommand := (&dto.ExecutionRequest{Command: fmt.Sprintf("cat %q", path)}).FullCommand() retrieveCommand := (&dto.ExecutionRequest{Command: fmt.Sprintf("cat %q", path)}).FullCommand()
// Improve: Instead of using io.Discard use a **fixed-sized** buffer. With that we could improve the error message. // Improve: Instead of using io.Discard use a **fixed-sized** buffer. With that we could improve the error message.
exitCode, err := r.api.ExecuteCommand(r.id, ctx, retrieveCommand, false, &nullio.Reader{}, content, io.Discard) exitCode, err := r.api.ExecuteCommand(r.id, ctx, retrieveCommand, false, privilegedExecution,
&nullio.Reader{}, content, io.Discard)
if err != nil { if err != nil {
return fmt.Errorf("%w: nomad error during retrieve file content copy: %v", return fmt.Errorf("%w: nomad error during retrieve file content copy: %v",

View File

@ -403,6 +403,6 @@ func NewRunner(id string, manager Accessor) Runner {
func (s *UpdateFileSystemTestSuite) TestGetFileContentReturnsErrorIfExitCodeIsNotZero() { func (s *UpdateFileSystemTestSuite) TestGetFileContentReturnsErrorIfExitCodeIsNotZero() {
s.mockedExecuteCommandCall.RunFn = nil s.mockedExecuteCommandCall.RunFn = nil
s.mockedExecuteCommandCall.Return(1, nil) s.mockedExecuteCommandCall.Return(1, nil)
err := s.runner.GetFileContent("", &bytes.Buffer{}, context.Background()) err := s.runner.GetFileContent("", &bytes.Buffer{}, false, context.Background())
s.ErrorIs(err, ErrFileNotFound) s.ErrorIs(err, ErrFileNotFound)
} }

View File

@ -46,7 +46,7 @@ type Runner interface {
// ListFileSystem streams the listing of the file system of the requested directory into the Writer provided. // ListFileSystem streams the listing of the file system of the requested directory into the Writer provided.
// The result is streamed via the io.Writer in order to not overload the memory with user input. // The result is streamed via the io.Writer in order to not overload the memory with user input.
ListFileSystem(path string, recursive bool, result io.Writer, ctx context.Context) error ListFileSystem(path string, recursive bool, result io.Writer, privilegedExecution bool, ctx context.Context) error
// UpdateFileSystem processes a dto.UpdateFileSystemRequest by first deleting each given dto.FilePath recursively // UpdateFileSystem processes a dto.UpdateFileSystemRequest by first deleting each given dto.FilePath recursively
// and then copying each given dto.File to the runner. // and then copying each given dto.File to the runner.
@ -54,7 +54,7 @@ type Runner interface {
// GetFileContent streams the file content at the requested path into the Writer provided at content. // GetFileContent streams the file content at the requested path into the Writer provided at content.
// The result is streamed via the io.Writer in order to not overload the memory with user input. // The result is streamed via the io.Writer in order to not overload the memory with user input.
GetFileContent(path string, content io.Writer, ctx context.Context) error GetFileContent(path string, content io.Writer, privilegedExecution bool, ctx context.Context) error
// Destroy destroys the Runner in Nomad. // Destroy destroys the Runner in Nomad.
Destroy() error Destroy() error

View File

@ -92,13 +92,13 @@ func (_m *RunnerMock) ExecutionExists(id string) bool {
return r0 return r0
} }
// GetFileContent provides a mock function with given fields: path, content, ctx // GetFileContent provides a mock function with given fields: path, content, privilegedExecution, ctx
func (_m *RunnerMock) GetFileContent(path string, content io.Writer, ctx context.Context) error { func (_m *RunnerMock) GetFileContent(path string, content io.Writer, privilegedExecution bool, ctx context.Context) error {
ret := _m.Called(path, content, ctx) ret := _m.Called(path, content, privilegedExecution, ctx)
var r0 error var r0 error
if rf, ok := ret.Get(0).(func(string, io.Writer, context.Context) error); ok { if rf, ok := ret.Get(0).(func(string, io.Writer, bool, context.Context) error); ok {
r0 = rf(path, content, ctx) r0 = rf(path, content, privilegedExecution, ctx)
} else { } else {
r0 = ret.Error(0) r0 = ret.Error(0)
} }
@ -120,13 +120,13 @@ func (_m *RunnerMock) ID() string {
return r0 return r0
} }
// ListFileSystem provides a mock function with given fields: path, recursive, result, ctx // ListFileSystem provides a mock function with given fields: path, recursive, result, privilegedExecution, ctx
func (_m *RunnerMock) ListFileSystem(path string, recursive bool, result io.Writer, ctx context.Context) error { func (_m *RunnerMock) ListFileSystem(path string, recursive bool, result io.Writer, privilegedExecution bool, ctx context.Context) error {
ret := _m.Called(path, recursive, result, ctx) ret := _m.Called(path, recursive, result, privilegedExecution, ctx)
var r0 error var r0 error
if rf, ok := ret.Get(0).(func(string, bool, io.Writer, context.Context) error); ok { if rf, ok := ret.Get(0).(func(string, bool, io.Writer, bool, context.Context) error); ok {
r0 = rf(path, recursive, result, ctx) r0 = rf(path, recursive, result, privilegedExecution, ctx)
} else { } else {
r0 = ret.Error(0) r0 = ret.Error(0)
} }