Skip to content

Commit b4a2115

Browse files
Display correct key path to user for agent options (#25199)
#24038
1 parent 8c338a1 commit b4a2115

File tree

5 files changed

+138
-0
lines changed

5 files changed

+138
-0
lines changed

changes/24038-agent-options-key-error

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Display the correct path for agent options when a key is placed in the wrong object

ee/server/service/teams.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,23 @@ func (svc *Service) ModifyTeamAgentOptions(ctx context.Context, teamID uint, tea
414414

415415
if teamOptions != nil {
416416
if err := fleet.ValidateJSONAgentOptions(ctx, svc.ds, teamOptions, true); err != nil {
417+
if field := fleet.GetJSONUnknownField(err); field != nil {
418+
correctKeyPath, keyErr := fleet.FindAgentOptionsKeyPath(*field)
419+
if keyErr != nil {
420+
level.Error(svc.logger).Log("err", err, "msg", "error parsing generated agent options structs")
421+
}
422+
var keyPathJoined string
423+
switch pathLen := len(correctKeyPath); {
424+
case pathLen > 1:
425+
keyPathJoined = fmt.Sprintf("%q", strings.Join(correctKeyPath[:len(correctKeyPath)-1], "."))
426+
case pathLen == 1:
427+
keyPathJoined = "top level"
428+
}
429+
if keyPathJoined != "" {
430+
err = fmt.Errorf("%q should be part of the %s object", *field, keyPathJoined)
431+
}
432+
}
433+
417434
err = fleet.NewUserMessageError(err, http.StatusBadRequest)
418435
if applyOptions.Force && !applyOptions.DryRun {
419436
level.Info(svc.logger).Log("err", err, "msg", "force-apply team agent options with validation errors")

server/fleet/agent_options.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,3 +325,88 @@ func validateJSONAgentOptionsSet(rawJSON json.RawMessage) error {
325325
}
326326
return nil
327327
}
328+
329+
func FindAgentOptionsKeyPath(key string) ([]string, error) {
330+
if key == "script_execution_timeout" {
331+
return []string{"script_execution_timeout"}, nil
332+
}
333+
334+
configPath, err := locateStructJSONKeyPath(key, "config", osqueryAgentOptions{})
335+
if err != nil {
336+
return nil, fmt.Errorf("locating key path in agent options: %w", err)
337+
}
338+
if configPath != nil {
339+
return configPath, nil
340+
}
341+
342+
if key == "overrides" {
343+
return []string{"overrides"}, nil
344+
}
345+
if key == "platforms" {
346+
return []string{"overrides", "platforms"}, nil
347+
}
348+
349+
commandLinePath, err := locateStructJSONKeyPath(key, "command_line_flags", osqueryCommandLineFlags{})
350+
if err != nil {
351+
return nil, fmt.Errorf("locating key path in agent command line options: %w", err)
352+
}
353+
if commandLinePath != nil {
354+
return commandLinePath, nil
355+
}
356+
357+
extensionsPath, err := locateStructJSONKeyPath(key, "extensions", ExtensionInfo{})
358+
if err != nil {
359+
return nil, fmt.Errorf("locating key path in agent extensions options: %w", err)
360+
}
361+
if extensionsPath != nil {
362+
return extensionsPath, nil
363+
}
364+
365+
channelsPath, err := locateStructJSONKeyPath(key, "update_channels", OrbitUpdateChannels{})
366+
if err != nil {
367+
return nil, fmt.Errorf("locating key path in agent update channels: %w", err)
368+
}
369+
if channelsPath != nil {
370+
return channelsPath, nil
371+
}
372+
373+
return nil, nil
374+
}
375+
376+
// Only searches two layers deep
377+
func locateStructJSONKeyPath(key, startKey string, target any) ([]string, error) {
378+
if key == startKey {
379+
return []string{startKey}, nil
380+
}
381+
382+
optionsBytes, err := json.Marshal(target)
383+
if err != nil {
384+
return nil, fmt.Errorf("unable to marshall target: %w", err)
385+
}
386+
387+
var opts map[string]any
388+
389+
if err := json.Unmarshal(optionsBytes, &opts); err != nil {
390+
return nil, fmt.Errorf("unable to unmarshall target: %w", err)
391+
}
392+
393+
var path [3]string
394+
path[0] = startKey
395+
for k, v := range opts {
396+
path[1] = k
397+
if k == key {
398+
return path[:2], nil
399+
}
400+
401+
if inner, ok := v.(map[string]any); ok {
402+
for k2 := range inner {
403+
path[2] = k2
404+
if key == k2 {
405+
return path[:3], nil
406+
}
407+
}
408+
}
409+
}
410+
411+
return nil, nil
412+
}

server/fleet/errors.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,15 @@ func IsJSONUnknownFieldError(err error) bool {
477477
return rxJSONUnknownField.MatchString(err.Error())
478478
}
479479

480+
func GetJSONUnknownField(err error) *string {
481+
errCause := Cause(err)
482+
if IsJSONUnknownFieldError(errCause) {
483+
substr := rxJSONUnknownField.FindStringSubmatch(errCause.Error())
484+
return &substr[1]
485+
}
486+
return nil
487+
}
488+
480489
// UserMessage implements the user-friendly translation of the error if its
481490
// root cause is one of the supported types, otherwise it returns the error
482491
// message.

server/service/integration_enterprise_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1476,6 +1476,32 @@ func (s *integrationEnterpriseTestSuite) TestTeamEndpoints() {
14761476
"x": "y"
14771477
}`), http.StatusBadRequest, &tmResp)
14781478

1479+
// modify team agent options with invalid key
1480+
badRes := s.Do("POST", fmt.Sprintf("/api/latest/fleet/teams/%d/agent_options", tm1ID), json.RawMessage(`{
1481+
"bad_key": 1
1482+
}`), http.StatusBadRequest)
1483+
errText := extractServerErrorText(badRes.Body)
1484+
require.Contains(t, errText, "unsupported key provided")
1485+
1486+
// modify team agent options with correct options under the wrong key
1487+
badRes = s.Do("POST", fmt.Sprintf("/api/latest/fleet/teams/%d/agent_options", tm1ID), json.RawMessage(`{
1488+
"distributed_tls_max_attempts": 3
1489+
}`), http.StatusBadRequest)
1490+
errText = extractServerErrorText(badRes.Body)
1491+
require.Contains(t, errText, "\"distributed_tls_max_attempts\" should be part of the \"config.options\" object")
1492+
1493+
badRes = s.Do("POST", fmt.Sprintf("/api/latest/fleet/teams/%d/agent_options", tm1ID), json.RawMessage(`{
1494+
"config": { "options": { "logger_plugin": 3 } }
1495+
}`), http.StatusBadRequest)
1496+
errText = extractServerErrorText(badRes.Body)
1497+
require.Contains(t, errText, "\"logger_plugin\" should be part of the \"command_line_flags\" object")
1498+
1499+
badRes = s.Do("POST", fmt.Sprintf("/api/latest/fleet/teams/%d/agent_options", tm1ID), json.RawMessage(`{
1500+
"update_channels": { "config": 1 }
1501+
}`), http.StatusBadRequest)
1502+
errText = extractServerErrorText(badRes.Body)
1503+
require.Contains(t, errText, "\"config\" should be part of the top level object")
1504+
14791505
// modify team agent options with invalid platform options
14801506
tmResp.Team = nil
14811507
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/teams/%d/agent_options", tm1ID), json.RawMessage(

0 commit comments

Comments
 (0)