diff --git a/ee/server/calendar/google_calendar.go b/ee/server/calendar/google_calendar.go index 73c47d5cde4..39c31d4807c 100644 --- a/ee/server/calendar/google_calendar.go +++ b/ee/server/calendar/google_calendar.go @@ -298,30 +298,30 @@ func (c *GoogleCalendar) Configure(userEmail string) error { } func (c *GoogleCalendar) UpdateEventBody(event *fleet.CalendarEvent, - genBodyFn fleet.CalendarGenBodyFn) error { + genBodyFn fleet.CalendarGenBodyFn) (string, error) { details, err := c.unmarshalDetails(event) if err != nil { - return err + return "", err } gEvent, err := c.config.API.GetEvent(details.ID, "") if err != nil { - return ctxerr.Wrap(c.config.Context, err, "retrieving Google calendar event") + return "", ctxerr.Wrap(c.config.Context, err, "retrieving Google calendar event") } // Check if the current description contains the conflict text conflict := strings.Contains(gEvent.Description, fleet.CalendarEventConflictText) var ok bool gEvent.Description, ok, err = genBodyFn(conflict) if err != nil { - return ctxerr.Wrap(c.config.Context, err, "generating calendar event body") + return "", ctxerr.Wrap(c.config.Context, err, "generating calendar event body") } if !ok { - return nil + return "", nil } - _, err = c.config.API.UpdateEvent(gEvent) + updatedEvent, err := c.config.API.UpdateEvent(gEvent) if err != nil { - return ctxerr.Wrap(c.config.Context, err, "updating Google calendar event") + return "", ctxerr.Wrap(c.config.Context, err, "updating Google calendar event") } - return nil + return updatedEvent.Etag, nil } func (c *GoogleCalendar) GetAndUpdateEvent(event *fleet.CalendarEvent, genBodyFn fleet.CalendarGenBodyFn, @@ -441,7 +441,8 @@ func (c *GoogleCalendar) GetAndUpdateEvent(event *fleet.CalendarEvent, genBodyFn if err != nil { return nil, false, err } - fleetEvent, err := c.googleEventToFleetEvent(*startTime, *endTime, gEvent, event.UUID, details.ChannelID, details.ResourceID) + fleetEvent, err := c.googleEventToFleetEvent(*startTime, *endTime, gEvent, event.UUID, details.ChannelID, details.ResourceID, + details.BodyTag) if err != nil { return nil, false, err } @@ -674,8 +675,8 @@ func (c *GoogleCalendar) createEvent( resourceID = opts.ResourceID } - // Convert Google event to Fleet event - fleetEvent, err := c.googleEventToFleetEvent(eventStart, eventEnd, event, eventUUID, channelID, resourceID) + // Convert Google event to Fleet event. Body tag will be updated by the calling function. + fleetEvent, err := c.googleEventToFleetEvent(eventStart, eventEnd, event, eventUUID, channelID, resourceID, "body_tag") if err != nil { return nil, err } @@ -726,8 +727,7 @@ func getLocation(tz string, config *GoogleCalendarConfig) *time.Location { } func (c *GoogleCalendar) googleEventToFleetEvent(startTime time.Time, endTime time.Time, event *calendar.Event, eventUUID string, - channelID string, - resourceID string) ( + channelID string, resourceID string, bodyTag string) ( *fleet.CalendarEvent, error, ) { tzName := c.location.String() @@ -742,6 +742,7 @@ func (c *GoogleCalendar) googleEventToFleetEvent(startTime time.Time, endTime ti ETag: event.Etag, ChannelID: channelID, ResourceID: resourceID, + BodyTag: bodyTag, } detailsJson, err := json.Marshal(details) if err != nil { diff --git a/ee/server/service/calendar.go b/ee/server/service/calendar.go index 4f5f971b56d..75886f10534 100644 --- a/ee/server/service/calendar.go +++ b/ee/server/service/calendar.go @@ -235,7 +235,9 @@ func (svc *Service) processCalendarEvent(ctx context.Context, eventDetails *flee return ctxerr.Wrap(ctx, err, "set recent update flag") } // Event was updated, so we need to save it - err = event.SaveBodyTag(generatedTag) + if generatedTag != "" { + err = event.SaveDataItems("body_tag", generatedTag) + } if err != nil { return ctxerr.Wrap(ctx, err, "save calendar event body tag") } diff --git a/server/cron/calendar_cron.go b/server/cron/calendar_cron.go index 4e717a84b6e..ed943caa520 100644 --- a/server/cron/calendar_cron.go +++ b/server/cron/calendar_cron.go @@ -396,6 +396,7 @@ func processFailingHostExistingCalendarEvent( // Function to generate calendar event body. var generatedTag string + var newETag string var genBodyFn fleet.CalendarGenBodyFn = func(conflict bool) (string, bool, error) { var body string body, generatedTag = calendar.GenerateCalendarEventBody(ctx, ds, orgName, host, policyIDtoPolicy, conflict, logger) @@ -409,7 +410,7 @@ func processFailingHostExistingCalendarEvent( updatedBodyTag := getBodyTag(ctx, ds, host, policyIDtoPolicy, logger) if currentBodyTag != updatedBodyTag && updatedBodyTag != "" { - err = userCalendar.UpdateEventBody(calendarEvent, genBodyFn) + newETag, err = userCalendar.UpdateEventBody(calendarEvent, genBodyFn) if err != nil { return fmt.Errorf("update event body: %w", err) } @@ -440,8 +441,8 @@ func processFailingHostExistingCalendarEvent( } if updated { - if generatedTag != "" { - err = updatedEvent.SaveBodyTag(generatedTag) + if generatedTag != "" && newETag != "" { + err = updatedEvent.SaveDataItems("body_tag", generatedTag, "etag", newETag) if err != nil { return fmt.Errorf("save calendar event body tag: %w", err) } @@ -623,7 +624,7 @@ func attemptCreatingEventOnUserCalendar( var dee fleet.DayEndedError switch { case err == nil: - err = calendarEvent.SaveBodyTag(generatedTag) + err = calendarEvent.SaveDataItems("body_tag", generatedTag) if err != nil { return nil, err } diff --git a/server/fleet/calendar.go b/server/fleet/calendar.go index 72c3e9e0ba3..fa9de8f07a9 100644 --- a/server/fleet/calendar.go +++ b/server/fleet/calendar.go @@ -42,8 +42,8 @@ type UserCalendar interface { GetAndUpdateEvent(event *CalendarEvent, genBodyFn CalendarGenBodyFn, opts CalendarGetAndUpdateEventOpts) (updatedEvent *CalendarEvent, updated bool, err error) - // UpdateEventBody updates the body of the calendar event. - UpdateEventBody(event *CalendarEvent, genBodyFn CalendarGenBodyFn) error + // UpdateEventBody updates the body of the calendar event and returns new ETag + UpdateEventBody(event *CalendarEvent, genBodyFn CalendarGenBodyFn) (string, error) // DeleteEvent deletes the event with the given ID. DeleteEvent(event *CalendarEvent) error // StopEventChannel stops the event's callback channel. diff --git a/server/fleet/calendar_events.go b/server/fleet/calendar_events.go index 44d11306fca..5e730e283b1 100644 --- a/server/fleet/calendar_events.go +++ b/server/fleet/calendar_events.go @@ -2,6 +2,7 @@ package fleet import ( "encoding/json" + "errors" "fmt" "time" ) @@ -30,7 +31,10 @@ func (ce *CalendarEvent) GetBodyTag() string { return d.BodyTag } -func (ce *CalendarEvent) SaveBodyTag(bodyTag string) error { +func (ce *CalendarEvent) SaveDataItems(keysAndValues ...string) error { + if len(keysAndValues)%2 != 0 { + return errors.New("SaveDataItem requires an even number of arguments") + } var result map[string]any if len(ce.Data) > 0 { err := json.Unmarshal(ce.Data, &result) @@ -40,7 +44,11 @@ func (ce *CalendarEvent) SaveBodyTag(bodyTag string) error { } else { result = make(map[string]any, 1) } - result["body_tag"] = bodyTag + for i := 0; i < len(keysAndValues); i += 2 { + key := keysAndValues[i] + value := keysAndValues[i+1] + result[key] = value + } data, err := json.Marshal(result) if err != nil { return fmt.Errorf("could not marshal event data: %w", err) diff --git a/server/fleet/calendar_events_test.go b/server/fleet/calendar_events_test.go index 3e79bd6bb49..931cd3889fd 100644 --- a/server/fleet/calendar_events_test.go +++ b/server/fleet/calendar_events_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestBodyTag(t *testing.T) { +func TestSaveDataItems(t *testing.T) { t.Parallel() var event CalendarEvent @@ -16,7 +16,7 @@ func TestBodyTag(t *testing.T) { assert.Equal(t, "", event.GetBodyTag()) bodyTag := "bodyTag" - require.NoError(t, event.SaveBodyTag(bodyTag)) + require.NoError(t, event.SaveDataItems("body_tag", bodyTag)) assert.Equal(t, bodyTag, event.GetBodyTag()) testMap := make(map[string]any, 5) @@ -29,11 +29,11 @@ func TestBodyTag(t *testing.T) { event.Data = data assert.Equal(t, oldBodyTag, event.GetBodyTag()) - require.NoError(t, event.SaveBodyTag(bodyTag)) + require.NoError(t, event.SaveDataItems("body_tag", bodyTag)) assert.Equal(t, bodyTag, event.GetBodyTag()) // Make sure data was not modified - require.NoError(t, event.SaveBodyTag(oldBodyTag)) + require.NoError(t, event.SaveDataItems("body_tag", oldBodyTag)) var result map[string]any require.NoError(t, json.Unmarshal(event.Data, &result)) assert.Equal(t, testMap, result) diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index f87d9b144ab..438138dc672 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -11728,11 +11728,15 @@ func (s *integrationEnterpriseTestSuite) TestCalendarCallback() { require.NotZero(t, event.StartTime) require.NotZero(t, event.EndTime) require.NotEmpty(t, event.UUID) + bodyTag := event.GetBodyTag() + assert.NotEmpty(t, bodyTag) assert.Equal(t, 1, calendar.MockChannelsCount()) // Get channel ID type eventDetails struct { ChannelID string `json:"channel_id"` + BodyTag string `json:"body_tag"` + ETag string `json:"etag"` } var details eventDetails err = json.Unmarshal(event.Data, &details) @@ -11855,6 +11859,8 @@ func (s *integrationEnterpriseTestSuite) TestCalendarCallback() { err = json.Unmarshal(eventRecreated.Data, &details) require.NoError(t, err) + assert.NotEmpty(t, details.BodyTag) + bodyTag = details.BodyTag // New event callback should work _ = s.DoRawWithHeaders("POST", "/api/v1/fleet/calendar/webhook/"+eventRecreated.UUID, []byte(""), http.StatusOK, @@ -11891,6 +11897,30 @@ func (s *integrationEnterpriseTestSuite) TestCalendarCallback() { assert.Greater(t, eventUpdated.StartTime, eventRecreated.StartTime) assert.Equal(t, eventRecreated.EndTime, eventUpdated.EndTime) assert.Equal(t, 1, calendar.MockChannelsCount()) + assert.Equal(t, bodyTag, eventRecreated.GetBodyTag()) + + // Change the body contents of event. + events = calendar.ListGoogleMockEvents() + require.Len(t, events, 1) + eTag := "description change etag" + for _, e := range events { + e.Etag = eTag + e.Description = "new description" + } + // New event callback should cause Etag to update but Body tag to remain the same + _ = s.DoRawWithHeaders("POST", "/api/v1/fleet/calendar/webhook/"+eventRecreated.UUID, []byte(""), http.StatusOK, + map[string]string{ + "X-Goog-Channel-Id": details.ChannelID, + "X-Goog-Resource-State": "exists", + }) + team1CalendarEvents, err = s.ds.ListCalendarEvents(ctx, &team1.ID) + require.NoError(t, err) + require.Len(t, team1CalendarEvents, 1) + eventDescUpdated := team1CalendarEvents[0] + err = json.Unmarshal(eventDescUpdated.Data, &details) + require.NoError(t, err) + assert.Equal(t, bodyTag, details.BodyTag) + assert.Equal(t, eTag, details.ETag) // Update the time of the event again events = calendar.ListGoogleMockEvents() @@ -11900,6 +11930,7 @@ func (s *integrationEnterpriseTestSuite) TestCalendarCallback() { require.NoError(t, err) newStartTime := st.Add(5 * time.Minute).Format(time.RFC3339) e.Start.DateTime = newStartTime + e.Etag = e.Etag + "1" } // Grab the lock @@ -11950,6 +11981,9 @@ func (s *integrationEnterpriseTestSuite) TestCalendarCallback() { require.NoError(t, err) if len(team1CalendarEvents) == 1 && team1CalendarEvents[0].UUID == event.UUID && team1CalendarEvents[0].StartTime.After(event.StartTime) { + err = json.Unmarshal(team1CalendarEvents[0].Data, &details) + require.NoError(t, err) + assert.NotEqual(t, eTag, details.ETag, "ETag should have updated") done <- struct{}{} return }