mirror of
https://github.com/openrport/openrport.git
synced 2025-10-26 11:27:11 +00:00
438 lines
11 KiB
Go
438 lines
11 KiB
Go
package chserver
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"io/ioutil"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/mock"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/openrport/openrport/server/api"
|
|
"github.com/openrport/openrport/server/api/users"
|
|
"github.com/openrport/openrport/server/chconfig"
|
|
"github.com/openrport/openrport/server/clients"
|
|
"github.com/openrport/openrport/server/clients/clientdata"
|
|
"github.com/openrport/openrport/share/files"
|
|
"github.com/openrport/openrport/share/models"
|
|
"github.com/openrport/openrport/share/test"
|
|
)
|
|
|
|
func MockUserService(user string, group string) *users.APIService {
|
|
curUser := &users.User{
|
|
Username: user,
|
|
Groups: []string{group},
|
|
}
|
|
return users.NewAPIService(users.NewStaticProvider([]*users.User{curUser}), false, 0, -1)
|
|
}
|
|
|
|
func FsCallback(fs *test.FileAPIMock, t *testing.T) {
|
|
fs.On("CreateDirIfNotExists", "/data/"+files.DefaultUploadTempFolder, files.DefaultMode).Return(true, nil)
|
|
|
|
fileExpectation := func(f io.Reader) bool {
|
|
actualFileContent, err := ioutil.ReadAll(f)
|
|
|
|
require.NoError(t, err)
|
|
|
|
return string(actualFileContent) == "some content"
|
|
}
|
|
fs.On("CreateFile", "/data/filepush/id-123_rport_filepush", mock.MatchedBy(fileExpectation)).Return(int64(10), nil)
|
|
|
|
fileMock := &test.ReadWriteCloserMock{}
|
|
fileMock.Reader = strings.NewReader("some content")
|
|
fileMock.On("Close").Return(nil)
|
|
|
|
fs.On("Open", "/data/filepush/id-123_rport_filepush").Return(fileMock, nil)
|
|
fs.On("Remove", "/data/filepush/id-123_rport_filepush").Return(nil)
|
|
}
|
|
|
|
func TestHandleFileUploads(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
wantStatus int
|
|
useFsCallback bool
|
|
wantResp *models.UploadResponseShort
|
|
wantClientInputFile *models.UploadedFile
|
|
fileName string
|
|
fileContent string
|
|
formParts map[string][]string
|
|
cl *clientdata.Client
|
|
clientTags []string
|
|
user string
|
|
group string
|
|
wantErrCode string
|
|
wantErrTitle string
|
|
wantErrDetail string
|
|
}{
|
|
{
|
|
name: "send file success",
|
|
wantStatus: http.StatusOK,
|
|
user: "admin",
|
|
group: users.Administrators,
|
|
wantResp: &models.UploadResponseShort{
|
|
ID: "id-123",
|
|
Filepath: "/destination/myfile.txt",
|
|
SizeBytes: 10,
|
|
},
|
|
useFsCallback: true,
|
|
fileName: "file.txt",
|
|
fileContent: "some content",
|
|
cl: clients.New(t).ID("22114341234").Logger(testLog).Build(),
|
|
formParts: map[string][]string{
|
|
"client_id": {
|
|
"22114341234",
|
|
},
|
|
"dest": {
|
|
"/destination/myfile.txt",
|
|
},
|
|
"id": {
|
|
"id-123",
|
|
},
|
|
"user": {
|
|
"admin",
|
|
},
|
|
"group": {
|
|
"group",
|
|
},
|
|
"mode": {
|
|
"0744",
|
|
},
|
|
"force": {
|
|
"1",
|
|
},
|
|
"sync": {
|
|
"1",
|
|
},
|
|
},
|
|
wantClientInputFile: &models.UploadedFile{
|
|
ID: "id-123",
|
|
SourceFilePath: "/data/filepush/id-123_rport_filepush",
|
|
DestinationPath: "/destination/myfile.txt",
|
|
DestinationFileMode: 0744,
|
|
DestinationFileOwner: "admin",
|
|
DestinationFileGroup: "group",
|
|
ForceWrite: true,
|
|
Sync: true,
|
|
Md5Checksum: test.Md5Hash("some content"),
|
|
},
|
|
},
|
|
{
|
|
name: "send file success targeting tags",
|
|
wantStatus: http.StatusOK,
|
|
user: "admin",
|
|
group: users.Administrators,
|
|
wantResp: &models.UploadResponseShort{
|
|
ID: "id-123",
|
|
Filepath: "/destination/myfile.txt",
|
|
SizeBytes: 10,
|
|
},
|
|
useFsCallback: true,
|
|
fileName: "file.txt",
|
|
fileContent: "some content",
|
|
cl: clients.New(t).ID("22114341234").Logger(testLog).Build(),
|
|
clientTags: []string{"linux"},
|
|
formParts: map[string][]string{
|
|
"tags": {
|
|
`{
|
|
"tags": ["linux"],
|
|
"operator": "OR"
|
|
}`,
|
|
},
|
|
"dest": {
|
|
"/destination/myfile.txt",
|
|
},
|
|
"id": {
|
|
"id-123",
|
|
},
|
|
"user": {
|
|
"admin",
|
|
},
|
|
"group": {
|
|
"group",
|
|
},
|
|
"mode": {
|
|
"0744",
|
|
},
|
|
"force": {
|
|
"1",
|
|
},
|
|
"sync": {
|
|
"1",
|
|
},
|
|
},
|
|
wantClientInputFile: &models.UploadedFile{
|
|
ID: "id-123",
|
|
SourceFilePath: "/data/filepush/id-123_rport_filepush",
|
|
DestinationPath: "/destination/myfile.txt",
|
|
DestinationFileMode: 0744,
|
|
DestinationFileOwner: "admin",
|
|
DestinationFileGroup: "group",
|
|
ForceWrite: true,
|
|
Sync: true,
|
|
Md5Checksum: test.Md5Hash("some content"),
|
|
},
|
|
},
|
|
{
|
|
name: "send file failed, multiple targeting params",
|
|
wantStatus: http.StatusBadRequest,
|
|
user: "admin",
|
|
group: "",
|
|
wantResp: &models.UploadResponseShort{},
|
|
useFsCallback: true,
|
|
fileName: "file.txt",
|
|
fileContent: "some content",
|
|
cl: clients.New(t).ID("22114341234").Logger(testLog).Build(),
|
|
formParts: map[string][]string{
|
|
"client_id": {
|
|
"22114341234",
|
|
},
|
|
"tags": {
|
|
`{
|
|
"tags": ["linux"],
|
|
"operator": "OR"
|
|
}`,
|
|
},
|
|
"dest": {
|
|
"/destination/myfile.txt",
|
|
},
|
|
"id": {
|
|
"id-123",
|
|
},
|
|
},
|
|
wantClientInputFile: &models.UploadedFile{},
|
|
wantErrTitle: "Multiple targeting parameters.",
|
|
wantErrDetail: "multiple targeting options are not supported. Please specify only one",
|
|
},
|
|
{
|
|
name: "send file failed, missing tags element",
|
|
wantStatus: http.StatusBadRequest,
|
|
user: "admin",
|
|
group: "",
|
|
wantResp: &models.UploadResponseShort{},
|
|
useFsCallback: true,
|
|
fileName: "file.txt",
|
|
fileContent: "some content",
|
|
cl: clients.New(t).ID("22114341234").Logger(testLog).Build(),
|
|
formParts: map[string][]string{
|
|
"dest": {
|
|
"/destination/myfile.txt",
|
|
},
|
|
"id": {
|
|
"id-123",
|
|
},
|
|
},
|
|
wantClientInputFile: &models.UploadedFile{},
|
|
wantErrTitle: "Missing targeting parameters.",
|
|
wantErrDetail: "please specify targeting options, such as client ids, groups ids or tags",
|
|
},
|
|
{
|
|
name: "send file failed, empty tags",
|
|
wantStatus: http.StatusBadRequest,
|
|
user: "admin",
|
|
group: "",
|
|
wantResp: &models.UploadResponseShort{},
|
|
useFsCallback: true,
|
|
fileName: "file.txt",
|
|
fileContent: "some content",
|
|
cl: clients.New(t).ID("22114341234").Logger(testLog).Build(),
|
|
formParts: map[string][]string{
|
|
"tags": {
|
|
`{
|
|
"tags": [],
|
|
"operator": "OR"
|
|
}`,
|
|
},
|
|
"dest": {
|
|
"/destination/myfile.txt",
|
|
},
|
|
"id": {
|
|
"id-123",
|
|
},
|
|
},
|
|
wantClientInputFile: &models.UploadedFile{},
|
|
wantErrTitle: "No tags specified.",
|
|
wantErrDetail: "please specify tags in the tags list",
|
|
},
|
|
{
|
|
name: "send file denied, bad user rights",
|
|
wantStatus: http.StatusForbidden,
|
|
user: "loser",
|
|
group: "",
|
|
wantResp: &models.UploadResponseShort{},
|
|
useFsCallback: true,
|
|
fileName: "file.txt",
|
|
fileContent: "some content",
|
|
cl: clients.New(t).ID("22114341234").Logger(testLog).Build(),
|
|
formParts: map[string][]string{
|
|
"client_id": {
|
|
"22114341234",
|
|
},
|
|
"dest": {
|
|
"/destination/myfile.txt",
|
|
},
|
|
"id": {
|
|
"id-123",
|
|
},
|
|
},
|
|
wantClientInputFile: &models.UploadedFile{},
|
|
wantErrCode: "ACCESS_CONTROL_VIOLATION",
|
|
wantErrTitle: "upload forbidden",
|
|
wantErrDetail: "Access denied to client(s) with ID(s): 22114341234",
|
|
},
|
|
{
|
|
name: "send file denied, bad destination",
|
|
wantStatus: http.StatusBadRequest,
|
|
user: "loser",
|
|
group: "",
|
|
wantResp: &models.UploadResponseShort{},
|
|
useFsCallback: true,
|
|
fileName: "file.txt",
|
|
fileContent: "some content",
|
|
cl: clients.New(t).ID("22114341234").Logger(testLog).Build(),
|
|
formParts: map[string][]string{
|
|
"client_id": {
|
|
"22114341234",
|
|
},
|
|
"dest": {
|
|
"/proc/myfile.txt",
|
|
},
|
|
"id": {
|
|
"id-123",
|
|
},
|
|
},
|
|
wantClientInputFile: &models.UploadedFile{},
|
|
wantErrCode: "BAD_DESTINATION",
|
|
wantErrTitle: "upload denied",
|
|
wantErrDetail: "uploads to /proc/ are forbidden",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
cl := tc.cl
|
|
|
|
if tc.clientTags != nil {
|
|
cl.SetTags(tc.clientTags)
|
|
}
|
|
|
|
connMock := test.NewConnMock()
|
|
|
|
connMock.ReturnOk = true
|
|
|
|
done := make(chan bool)
|
|
connMock.DoneChannel = done
|
|
|
|
cl.SetConnection(connMock)
|
|
|
|
fileAPIMock := test.NewFileAPIMock()
|
|
if tc.useFsCallback {
|
|
FsCallback(fileAPIMock, t)
|
|
}
|
|
|
|
al := APIListener{
|
|
insecureForTests: true,
|
|
Server: &Server{
|
|
clientService: clients.NewClientService(
|
|
nil,
|
|
nil,
|
|
clients.NewClientRepository([]*clientdata.Client{cl}, &hour, testLog),
|
|
testLog,
|
|
nil,
|
|
),
|
|
clientGroupProvider: mockClientGroupProvider{},
|
|
config: &chconfig.Config{
|
|
Server: chconfig.ServerConfig{
|
|
DataDir: "/data",
|
|
},
|
|
API: chconfig.APIConfig{
|
|
MaxFilePushSize: int64(10 << 20),
|
|
},
|
|
},
|
|
filesAPI: fileAPIMock,
|
|
},
|
|
Logger: testLog,
|
|
userService: MockUserService(tc.user, tc.group),
|
|
}
|
|
|
|
al.initRouter()
|
|
|
|
body := &bytes.Buffer{}
|
|
writer := multipart.NewWriter(body)
|
|
|
|
part, err := writer.CreateFormFile("upload", tc.fileName)
|
|
require.NoError(t, err)
|
|
|
|
_, err = io.Copy(part, strings.NewReader(tc.fileContent))
|
|
require.NoError(t, err)
|
|
|
|
for key, vals := range tc.formParts {
|
|
for _, val := range vals {
|
|
err = writer.WriteField(key, val)
|
|
require.NoError(t, err)
|
|
}
|
|
}
|
|
|
|
err = writer.Close()
|
|
require.NoError(t, err)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/files", body)
|
|
req.Header.Add("Content-Type", writer.FormDataContentType())
|
|
ctx := api.WithUser(context.Background(), tc.user)
|
|
req = req.WithContext(ctx)
|
|
|
|
rec := httptest.NewRecorder()
|
|
al.router.ServeHTTP(rec, req)
|
|
|
|
t.Logf("Got response %s", rec.Body)
|
|
assert.Equal(t, tc.wantStatus, rec.Code)
|
|
if tc.wantErrTitle != "" {
|
|
wantResp := api.NewErrAPIPayloadFromMessage(tc.wantErrCode, tc.wantErrTitle, tc.wantErrDetail)
|
|
wantRespBytes, err := json.Marshal(wantResp)
|
|
require.NoError(t, err)
|
|
require.Equal(t, string(wantRespBytes), rec.Body.String())
|
|
return
|
|
}
|
|
|
|
var successResp struct {
|
|
Data *models.UploadResponseShort `json:"data"`
|
|
}
|
|
|
|
dec := json.NewDecoder(rec.Body)
|
|
dec.DisallowUnknownFields()
|
|
err = dec.Decode(&successResp)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, tc.wantResp, successResp.Data)
|
|
|
|
select {
|
|
case <-done:
|
|
assertClientPayload(t, connMock, tc.wantClientInputFile)
|
|
case <-time.After(time.Second * 2):
|
|
assertClientPayload(t, connMock, tc.wantClientInputFile)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func assertClientPayload(t *testing.T, connMock *test.ConnMock, wantClientInputFile *models.UploadedFile) {
|
|
name, wantReply, payload := connMock.InputSendRequest()
|
|
|
|
actualInputFile := &models.UploadedFile{}
|
|
err := actualInputFile.FromBytes(payload)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, "upload", name)
|
|
assert.Equal(t, wantClientInputFile, actualInputFile)
|
|
assert.True(t, wantReply)
|
|
}
|