mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 17:05:18 +00:00
3757aace08
#8129 Apart from fixing the issue in #8129, this change also introduces UUIDs to Fleet errors. To be able to match a returned error from the API to a error in the Fleet logs. See https://fleetdm.slack.com/archives/C019WG4GH0A/p1677780622769939 for more context. Samples with the changes in this PR: ``` curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d '' { "message": "Bad request", "errors": [ { "name": "base", "reason": "Expected JSON Body" } ], "uuid": "a01f6e10-354c-4ff0-b96e-1f64adb500b0" } ``` ``` curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d 'asd' { "message": "Bad request", "errors": [ { "name": "base", "reason": "json decoder error" } ], "uuid": "5f716a64-7550-464b-a1dd-e6a505a9f89d" } ``` ``` curl -k -X GET -H "Authorization: Bearer badtoken" "https://localhost:8080/api/latest/fleet/teams" { "message": "Authentication required", "errors": [ { "name": "base", "reason": "Authentication required" } ], "uuid": "efe45bc0-f956-4bf9-ba4f-aa9020a9aaaf" } ``` ``` curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}' { "message": "Authorization header required", "errors": [ { "name": "base", "reason": "Authorization header required" } ], "uuid": "57f78cd0-4559-464f-9df7-36c9ef7c89b3" } ``` ``` curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}' { "message": "Permission Denied", "uuid": "7f0220ad-6de7-4faf-8b6c-8d7ff9d2ca06" } ``` - [X] Changes file added for user-visible changes in `changes/` or `orbit/changes/`. See [Changes files](https://fleetdm.com/docs/contributing/committing-changes#changes-files) for more information. - [X] Documented any API changes (docs/Using-Fleet/REST-API.md or docs/Contributing/API-for-contributors.md) - ~[ ] Documented any permissions changes~ - ~[ ] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements)~ - ~[ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for new osquery data ingestion features.~ - [X] Added/updated tests - [X] Manual QA for all new/changed functionality - For Orbit and Fleet Desktop changes: - [X] Manual QA must be performed in the three main OSs, macOS, Windows and Linux. - ~[ ] Auto-update manual QA, from released version of component to new version (see [tools/tuf/test](../tools/tuf/test/README.md)).~
304 lines
8.6 KiB
Go
304 lines
8.6 KiB
Go
package service
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/gzip"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
"github.com/fleetdm/fleet/v4/server/ptr"
|
|
"github.com/go-kit/kit/log"
|
|
"github.com/go-kit/kit/log/level"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/stretchr/testify/suite"
|
|
)
|
|
|
|
func TestIntegrationLoggerTestSuite(t *testing.T) {
|
|
testingSuite := new(integrationLoggerTestSuite)
|
|
testingSuite.s = &testingSuite.Suite
|
|
suite.Run(t, testingSuite)
|
|
}
|
|
|
|
type integrationLoggerTestSuite struct {
|
|
withServer
|
|
suite.Suite
|
|
|
|
buf *bytes.Buffer
|
|
}
|
|
|
|
func (s *integrationLoggerTestSuite) SetupSuite() {
|
|
s.withDS.SetupSuite("integrationLoggerTestSuite")
|
|
|
|
s.buf = new(bytes.Buffer)
|
|
logger := log.NewJSONLogger(s.buf)
|
|
logger = level.NewFilter(logger, level.AllowDebug())
|
|
|
|
users, server := RunServerForTestsWithDS(s.T(), s.ds, &TestServerOpts{Logger: logger})
|
|
s.server = server
|
|
s.users = users
|
|
}
|
|
|
|
func (s *integrationLoggerTestSuite) TearDownTest() {
|
|
s.buf.Reset()
|
|
}
|
|
|
|
func (s *integrationLoggerTestSuite) TestLogger() {
|
|
t := s.T()
|
|
|
|
s.token = getTestAdminToken(t, s.server)
|
|
|
|
s.getConfig()
|
|
|
|
params := fleet.QueryPayload{
|
|
Name: ptr.String("somequery"),
|
|
Description: ptr.String("desc"),
|
|
Query: ptr.String("select 1 from osquery;"),
|
|
}
|
|
var createResp createQueryResponse
|
|
s.DoJSON("POST", "/api/latest/fleet/queries", params, http.StatusOK, &createResp)
|
|
|
|
logs := s.buf.String()
|
|
parts := strings.Split(strings.TrimSpace(logs), "\n")
|
|
assert.Len(t, parts, 3)
|
|
for i, part := range parts {
|
|
kv := make(map[string]string)
|
|
err := json.Unmarshal([]byte(part), &kv)
|
|
require.NoError(t, err)
|
|
|
|
assert.NotEqual(t, "", kv["took"])
|
|
|
|
switch i {
|
|
case 0:
|
|
assert.Equal(t, "info", kv["level"])
|
|
assert.Equal(t, "POST", kv["method"])
|
|
assert.Equal(t, "/api/latest/fleet/login", kv["uri"])
|
|
case 1:
|
|
assert.Equal(t, "debug", kv["level"])
|
|
assert.Equal(t, "GET", kv["method"])
|
|
assert.Equal(t, "/api/latest/fleet/config", kv["uri"])
|
|
assert.Equal(t, "admin1@example.com", kv["user"])
|
|
case 2:
|
|
assert.Equal(t, "debug", kv["level"])
|
|
assert.Equal(t, "POST", kv["method"])
|
|
assert.Equal(t, "/api/latest/fleet/queries", kv["uri"])
|
|
assert.Equal(t, "admin1@example.com", kv["user"])
|
|
assert.Equal(t, "somequery", kv["name"])
|
|
assert.Equal(t, "select 1 from osquery;", kv["sql"])
|
|
default:
|
|
t.Fail()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *integrationLoggerTestSuite) TestLoggerLogin() {
|
|
t := s.T()
|
|
|
|
type logEntry struct {
|
|
key string
|
|
val string
|
|
}
|
|
|
|
testCases := []struct {
|
|
loginRequest loginRequest
|
|
expectedStatus int
|
|
expectedLogs []logEntry
|
|
}{
|
|
{
|
|
loginRequest: loginRequest{Email: testUsers["admin1"].Email, Password: testUsers["admin1"].PlaintextPassword},
|
|
expectedStatus: http.StatusOK,
|
|
expectedLogs: []logEntry{{"email", testUsers["admin1"].Email}},
|
|
},
|
|
{
|
|
loginRequest: loginRequest{Email: testUsers["admin1"].Email, Password: "n074v411dp455w02d"},
|
|
expectedStatus: http.StatusUnauthorized,
|
|
expectedLogs: []logEntry{
|
|
{"email", testUsers["admin1"].Email},
|
|
{"level", "error"},
|
|
{"internal", "invalid password"},
|
|
},
|
|
},
|
|
{
|
|
loginRequest: loginRequest{Email: "h4x0r@3x4mp13.c0m", Password: "n074v411dp455w02d"},
|
|
expectedStatus: http.StatusUnauthorized,
|
|
expectedLogs: []logEntry{
|
|
{"email", "h4x0r@3x4mp13.c0m"},
|
|
{"level", "error"},
|
|
{"internal", "user not found"},
|
|
},
|
|
},
|
|
}
|
|
var resp loginResponse
|
|
for _, tt := range testCases {
|
|
s.DoJSON("POST", "/api/latest/fleet/login", tt.loginRequest, tt.expectedStatus, &resp)
|
|
logString := s.buf.String()
|
|
parts := strings.Split(strings.TrimSpace(logString), "\n")
|
|
require.Len(t, parts, 1)
|
|
logData := make(map[string]string)
|
|
require.NoError(t, json.Unmarshal([]byte(parts[0]), &logData))
|
|
|
|
require.NotContains(t, logData, "user") // logger context is set to skip user
|
|
|
|
for _, e := range tt.expectedLogs {
|
|
assert.Equal(t, logData[e.key], e.val)
|
|
}
|
|
s.buf.Reset()
|
|
}
|
|
}
|
|
|
|
func (s *integrationLoggerTestSuite) TestOsqueryEndpointsLogErrors() {
|
|
t := s.T()
|
|
|
|
_, err := s.ds.NewHost(context.Background(), &fleet.Host{
|
|
DetailUpdatedAt: time.Now(),
|
|
LabelUpdatedAt: time.Now(),
|
|
PolicyUpdatedAt: time.Now(),
|
|
SeenTime: time.Now(),
|
|
NodeKey: ptr.String(t.Name() + "1234"),
|
|
UUID: "1",
|
|
Hostname: "foo.local",
|
|
OsqueryHostID: ptr.String(t.Name()),
|
|
PrimaryIP: "192.168.1.1",
|
|
PrimaryMac: "30-65-EC-6F-C4-58",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
requestBody := io.NopCloser(bytes.NewBuffer([]byte(`{"node_key":"1234","log_type":"status","data":[}`)))
|
|
req, _ := http.NewRequest("POST", s.server.URL+"/api/osquery/log", requestBody)
|
|
client := fleethttp.NewClient()
|
|
resp, err := client.Do(req)
|
|
require.NoError(t, err)
|
|
jsn := struct {
|
|
Message string `json:"message"`
|
|
Errs []map[string]string `json:"errors,omitempty"`
|
|
UUID string `json:"uuid"`
|
|
}{}
|
|
err = json.NewDecoder(resp.Body).Decode(&jsn)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "Bad request", jsn.Message)
|
|
assert.Len(t, jsn.Errs, 1)
|
|
assert.Equal(t, "base", jsn.Errs[0]["name"])
|
|
assert.Equal(t, "json decoder error", jsn.Errs[0]["reason"])
|
|
require.NotEmpty(t, jsn.UUID)
|
|
|
|
logString := s.buf.String()
|
|
assert.Contains(t, logString, `invalid character '}' looking for beginning of value","level":"info","path":"/api/osquery/log","uuid":"`+jsn.UUID+`"}`, logString)
|
|
}
|
|
|
|
func (s *integrationLoggerTestSuite) TestSubmitLog() {
|
|
t := s.T()
|
|
|
|
h, err := s.ds.NewHost(context.Background(), &fleet.Host{
|
|
DetailUpdatedAt: time.Now(),
|
|
LabelUpdatedAt: time.Now(),
|
|
PolicyUpdatedAt: time.Now(),
|
|
SeenTime: time.Now(),
|
|
NodeKey: ptr.String(t.Name() + "1234"),
|
|
UUID: "1",
|
|
Hostname: "foo.local",
|
|
PrimaryIP: "192.168.1.1",
|
|
PrimaryMac: "30-65-EC-6F-C4-58",
|
|
OsqueryHostID: ptr.String(t.Name()),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// submit status logs
|
|
req := submitLogsRequest{
|
|
NodeKey: *h.NodeKey,
|
|
LogType: "status",
|
|
Data: nil,
|
|
}
|
|
res := submitLogsResponse{}
|
|
s.DoJSON("POST", "/api/osquery/log", req, http.StatusOK, &res)
|
|
|
|
logString := s.buf.String()
|
|
assert.Equal(t, 1, strings.Count(logString, `"ip_addr"`))
|
|
assert.Equal(t, 1, strings.Count(logString, "x_for_ip_addr"))
|
|
s.buf.Reset()
|
|
|
|
// submit results logs
|
|
req = submitLogsRequest{
|
|
NodeKey: *h.NodeKey,
|
|
LogType: "result",
|
|
Data: nil,
|
|
}
|
|
res = submitLogsResponse{}
|
|
s.DoJSON("POST", "/api/osquery/log", req, http.StatusOK, &res)
|
|
|
|
logString = s.buf.String()
|
|
assert.Equal(t, 1, strings.Count(logString, `"ip_addr"`))
|
|
assert.Equal(t, 1, strings.Count(logString, "x_for_ip_addr"))
|
|
s.buf.Reset()
|
|
|
|
// submit invalid type logs
|
|
req = submitLogsRequest{
|
|
NodeKey: *h.NodeKey,
|
|
LogType: "unknown",
|
|
Data: nil,
|
|
}
|
|
var errRes map[string]string
|
|
s.DoJSON("POST", "/api/osquery/log", req, http.StatusInternalServerError, &errRes)
|
|
assert.Contains(t, errRes["error"], "unknown log type")
|
|
s.buf.Reset()
|
|
|
|
// submit gzip-encoded request
|
|
var body bytes.Buffer
|
|
gw := gzip.NewWriter(&body)
|
|
_, err = fmt.Fprintf(gw, `{
|
|
"node_key": %q,
|
|
"log_type": "status",
|
|
"data": null
|
|
}`, *h.NodeKey)
|
|
require.NoError(t, err)
|
|
require.NoError(t, gw.Close())
|
|
|
|
s.DoRawWithHeaders("POST", "/api/osquery/log", body.Bytes(), http.StatusOK, map[string]string{"Content-Encoding": "gzip"})
|
|
logString = s.buf.String()
|
|
assert.Equal(t, 1, strings.Count(logString, `"ip_addr"`))
|
|
assert.Equal(t, 1, strings.Count(logString, "x_for_ip_addr"))
|
|
|
|
// submit same payload without specifying gzip encoding fails
|
|
s.DoRawWithHeaders("POST", "/api/osquery/log", body.Bytes(), http.StatusBadRequest, nil)
|
|
}
|
|
|
|
func (s *integrationLoggerTestSuite) TestEnrollAgentLogsErrors() {
|
|
t := s.T()
|
|
_, err := s.ds.NewHost(context.Background(), &fleet.Host{
|
|
DetailUpdatedAt: time.Now(),
|
|
LabelUpdatedAt: time.Now(),
|
|
PolicyUpdatedAt: time.Now(),
|
|
SeenTime: time.Now(),
|
|
NodeKey: ptr.String("1234"),
|
|
UUID: "1",
|
|
Hostname: "foo.local",
|
|
PrimaryIP: "192.168.1.1",
|
|
PrimaryMac: "30-65-EC-6F-C4-58",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
j, err := json.Marshal(&enrollAgentRequest{
|
|
EnrollSecret: "1234",
|
|
HostIdentifier: "4321",
|
|
HostDetails: nil,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
s.DoRawNoAuth("POST", "/api/osquery/enroll", j, http.StatusUnauthorized)
|
|
|
|
parts := strings.Split(strings.TrimSpace(s.buf.String()), "\n")
|
|
require.Len(t, parts, 1)
|
|
logData := make(map[string]json.RawMessage)
|
|
require.NoError(t, json.Unmarshal([]byte(parts[0]), &logData))
|
|
assert.Equal(t, `"error"`, string(logData["level"]))
|
|
assert.Contains(t, string(logData["err"]), `"enroll failed:`)
|
|
assert.Contains(t, string(logData["err"]), `no matching secret found`)
|
|
}
|