diff --git a/ee/server/calendar/calendar.go b/ee/server/calendar/calendar.go deleted file mode 100644 index 469437f3a0..0000000000 --- a/ee/server/calendar/calendar.go +++ /dev/null @@ -1,16 +0,0 @@ -package calendar - -import ( - "context" - "github.com/fleetdm/fleet/v4/server/fleet" - "github.com/go-kit/kit/log" -) - -type GoogleCalendarConfig struct { - Context context.Context - IntegrationConfig *fleet.GoogleCalendarIntegration - UserEmail string - Logger log.Logger - // Should be nil for production - API GoogleCalendarAPI -} diff --git a/ee/server/calendar/google_calendar.go b/ee/server/calendar/google_calendar.go index ff3d2dfb80..ce1e9cfb9a 100644 --- a/ee/server/calendar/google_calendar.go +++ b/ee/server/calendar/google_calendar.go @@ -5,16 +5,18 @@ import ( "encoding/json" "errors" "fmt" + "net/http" + "time" + "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "github.com/fleetdm/fleet/v4/server/fleet" + kitlog "github.com/go-kit/log" "github.com/go-kit/log/level" "golang.org/x/oauth2/google" "golang.org/x/oauth2/jwt" "google.golang.org/api/calendar/v3" "google.golang.org/api/googleapi" "google.golang.org/api/option" - "net/http" - "time" ) const ( @@ -30,15 +32,34 @@ var calendarScopes = []string{ "https://www.googleapis.com/auth/calendar.settings.readonly", } -// GoogleCalendar is an implementation of the Calendar interface that uses the +type GoogleCalendarConfig struct { + Context context.Context + IntegrationConfig *fleet.GoogleCalendarIntegration + Logger kitlog.Logger + // Should be nil for production + API GoogleCalendarAPI +} + +// GoogleCalendar is an implementation of the UserCalendar interface that uses the // Google Calendar API to manage events. type GoogleCalendar struct { - config *GoogleCalendarConfig - timezoneOffset *int + config *GoogleCalendarConfig + currentUserEmail string + timezoneOffset *int +} + +func NewGoogleCalendar(config *GoogleCalendarConfig) *GoogleCalendar { + if config.API == nil { + var lowLevelAPI GoogleCalendarAPI = &GoogleCalendarLowLevelAPI{} + config.API = lowLevelAPI + } + return &GoogleCalendar{ + config: config, + } } type GoogleCalendarAPI interface { - Connect(ctx context.Context, email, privateKey, subject string) error + Configure(ctx context.Context, serviceAccountEmail, privateKey, userToImpersonateEmail string) error GetSetting(name string) (*calendar.Setting, error) ListEvents(timeMin, timeMax string) (*calendar.Events, error) CreateEvent(event *calendar.Event) (*calendar.Event, error) @@ -55,15 +76,17 @@ type GoogleCalendarLowLevelAPI struct { service *calendar.Service } -// Connect creates a new Google Calendar service using the provided credentials. -func (lowLevelAPI *GoogleCalendarLowLevelAPI) Connect(ctx context.Context, email, privateKey, subject string) error { +// Configure creates a new Google Calendar service using the provided credentials. +func (lowLevelAPI *GoogleCalendarLowLevelAPI) Configure( + ctx context.Context, serviceAccountEmail, privateKey, userToImpersonateEmail string, +) error { // Create a new calendar service conf := &jwt.Config{ - Email: email, + Email: serviceAccountEmail, Scopes: calendarScopes, PrivateKey: []byte(privateKey), TokenURL: google.JWTTokenURL, - Subject: subject, + Subject: userToImpersonateEmail, } client := conf.Client(ctx) service, err := calendar.NewService(ctx, option.WithHTTPClient(client)) @@ -95,33 +118,18 @@ func (lowLevelAPI *GoogleCalendarLowLevelAPI) DeleteEvent(id string) error { return lowLevelAPI.service.Events.Delete(calendarID, id).Do() } -func (c *GoogleCalendar) Connect(config any) (fleet.Calendar, error) { - gConfig, ok := config.(*GoogleCalendarConfig) - if !ok { - return nil, errors.New("invalid Google calendar config") - } - if gConfig.API == nil { - var lowLevelAPI GoogleCalendarAPI = &GoogleCalendarLowLevelAPI{} - gConfig.API = lowLevelAPI - } - err := gConfig.API.Connect( - gConfig.Context, gConfig.IntegrationConfig.Email, gConfig.IntegrationConfig.PrivateKey, gConfig.UserEmail, +func (c *GoogleCalendar) Configure(userEmail string) error { + err := c.config.API.Configure( + c.config.Context, c.config.IntegrationConfig.Email, c.config.IntegrationConfig.PrivateKey, userEmail, ) if err != nil { - return nil, ctxerr.Wrap(gConfig.Context, err, "creating Google calendar service") + return ctxerr.Wrap(c.config.Context, err, "creating Google calendar service") } - - gCal := &GoogleCalendar{ - config: gConfig, - } - - return gCal, nil + c.currentUserEmail = userEmail + return nil } func (c *GoogleCalendar) GetAndUpdateEvent(event *fleet.CalendarEvent, genBodyFn func() string) (*fleet.CalendarEvent, bool, error) { - if c.config == nil { - return nil, false, errors.New("the Google calendar is not connected. Please call Connect first") - } if event.EndTime.Before(time.Now()) { return nil, false, ctxerr.Errorf(c.config.Context, "cannot get and update an event that has already ended: %s", event.EndTime) } @@ -199,16 +207,11 @@ func (c *GoogleCalendar) unmarshalDetails(event *fleet.CalendarEvent) (*eventDet if details.ID == "" { return nil, ctxerr.Errorf(c.config.Context, "missing Google calendar event ID") } - if details.ETag == "" { - return nil, ctxerr.Errorf(c.config.Context, "missing Google calendar event ETag") - } + // ETag is optional, but we need it to check if the event was modified return &details, nil } func (c *GoogleCalendar) CreateEvent(dayOfEvent time.Time, body string) (*fleet.CalendarEvent, error) { - if c.config == nil { - return nil, errors.New("the Google calendar is not connected. Please call Connect first") - } if c.timezoneOffset == nil { err := getTimezone(c) if err != nil { @@ -257,7 +260,7 @@ func (c *GoogleCalendar) CreateEvent(dayOfEvent time.Time, body string) (*fleet. attending = true } else { for _, attendee := range gEvent.Attendees { - if attendee.Email == c.config.UserEmail { + if attendee.Email == c.currentUserEmail { if attendee.ResponseStatus != "declined" { attending = true } @@ -316,8 +319,7 @@ func (c *GoogleCalendar) CreateEvent(dayOfEvent time.Time, body string) (*fleet. if err != nil { return nil, err } - level.Debug(c.config.Logger).Log("msg", "created Google calendar events", "user", c.config.UserEmail, "startTime", eventStart) - fmt.Printf("VICTOR Created event with id:%s and ETag:%s\n", event.Id, event.Etag) + level.Debug(c.config.Logger).Log("msg", "created Google calendar event", "user", c.currentUserEmail, "startTime", eventStart) return fleetEvent, nil } @@ -364,7 +366,7 @@ func (c *GoogleCalendar) googleEventToFleetEvent(startTime time.Time, endTime ti fleetEvent := &fleet.CalendarEvent{} fleetEvent.StartTime = startTime fleetEvent.EndTime = endTime - fleetEvent.Email = c.config.UserEmail + fleetEvent.Email = c.currentUserEmail details := &eventDetails{ ID: event.Id, ETag: event.Etag, @@ -379,7 +381,7 @@ func (c *GoogleCalendar) googleEventToFleetEvent(startTime time.Time, endTime ti func (c *GoogleCalendar) DeleteEvent(event *fleet.CalendarEvent) error { if c.config == nil { - return errors.New("the Google calendar is not connected. Please call Connect first") + return errors.New("the Google calendar is not connected. Please call Configure first") } details, err := c.unmarshalDetails(event) if err != nil { diff --git a/server/fleet/calendar.go b/server/fleet/calendar.go index dc682e24ea..db2bbbc45e 100644 --- a/server/fleet/calendar.go +++ b/server/fleet/calendar.go @@ -10,15 +10,16 @@ func (e DayEndedError) Error() string { return e.Msg } -type Calendar interface { - // Connect to calendar. This method must be called first. Currently, config must be a *GoogleCalendarConfig - Connect(config any) (Calendar, error) +type UserCalendar interface { + // Configure configures the connection to a user's calendar. Once configured, + // CreateEvent, GetAndUpdateEvent and DeleteEvent reference the user's calendar. + Configure(userEmail string) error + // CreateEvent creates a new event on the calendar on the given date. DayEndedError is returned if there is no time left on the given date to schedule event. + CreateEvent(dateOfEvent time.Time, body string) (event *CalendarEvent, err error) // GetAndUpdateEvent retrieves the event from the calendar. // If the event has been modified, it returns the updated event. // If the event has been deleted, it schedules a new event with given body callback and returns the new event. GetAndUpdateEvent(event *CalendarEvent, genBodyFn func() string) (updatedEvent *CalendarEvent, updated bool, err error) - // CreateEvent creates a new event on the calendar on the given date. DayEndedError is returned if there is no time left on the given date to schedule event. - CreateEvent(dateOfEvent time.Time, body string) (event *CalendarEvent, err error) // DeleteEvent deletes the event with the given ID. DeleteEvent(event *CalendarEvent) error }