mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 09:28:54 +00:00
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #37806 Removed `ds.ListActivities` from the legacy datastore and updated code/tests to use the new activity bounded context instead. The changes to `cron.go` and most changes to `mysql/activities_test.go` will eventually be migrated to the activity bounded context. The current changes are an intermediate step. The issues tracked by https://github.com/fleetdm/fleet/issues/38234 will be addressed in additional/parallel PRs shortly. # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. - Done in the previous PR ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Refactor** * Migrated activity retrieval from direct datastore calls to a service-based architecture for improved maintainability and consistency. * Enhanced system context handling for background automation tasks to ensure proper authorization during scheduled operations. * Streamlined activity recording for automated processes with dedicated system identity tracking. * **Tests** * Updated test infrastructure with new helpers for activity service integration across test suites. <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Ian Littman <iansltx@gmail.com>
113 lines
3.5 KiB
Go
113 lines
3.5 KiB
Go
package activities
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/cenkalti/backoff"
|
|
"github.com/fleetdm/fleet/v4/server"
|
|
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
"github.com/fleetdm/fleet/v4/server/ptr"
|
|
kithttp "github.com/go-kit/kit/transport/http"
|
|
kitlog "github.com/go-kit/log"
|
|
"github.com/go-kit/log/level"
|
|
)
|
|
|
|
type activityModule struct {
|
|
repo ActivityStore
|
|
logger kitlog.Logger
|
|
}
|
|
|
|
type ActivityModule interface {
|
|
NewActivity(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error
|
|
}
|
|
|
|
// ActivityStore is the datastore interface needed to handle Fleet activities.
|
|
// It is implemented by fleet.Datastore.
|
|
type ActivityStore interface {
|
|
AppConfig(ctx context.Context) (*fleet.AppConfig, error)
|
|
NewActivity(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time) error
|
|
}
|
|
|
|
func NewActivityModule(repo ActivityStore, logger kitlog.Logger) ActivityModule {
|
|
return &activityModule{
|
|
repo: repo,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
func (a *activityModule) NewActivity(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
|
|
appConfig, err := a.repo.AppConfig(ctx)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "get app config")
|
|
}
|
|
|
|
detailsBytes, err := json.Marshal(activity)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "marshaling activity details")
|
|
}
|
|
timestamp := time.Now()
|
|
|
|
if appConfig.WebhookSettings.ActivitiesWebhook.Enable {
|
|
webhookURL := appConfig.WebhookSettings.ActivitiesWebhook.DestinationURL
|
|
var userID *uint
|
|
var userName *string
|
|
var userEmail *string
|
|
activityType := activity.ActivityName()
|
|
|
|
if user != nil {
|
|
// To support creating activities with users that were deleted. This can happen
|
|
// for automatically installed software which uses the author of the upload as the author of
|
|
// the installation.
|
|
if user.ID != 0 && !user.Deleted {
|
|
userID = &user.ID
|
|
}
|
|
userName = &user.Name
|
|
userEmail = &user.Email
|
|
} else if automatableActivity, ok := activity.(fleet.AutomatableActivity); ok && automatableActivity.WasFromAutomation() {
|
|
userName = ptr.String(fleet.ActivityAutomationAuthor)
|
|
}
|
|
|
|
// TODO: webhook module? probably webhook job too tbh since this isn't very resilient
|
|
go func() {
|
|
retryStrategy := backoff.NewExponentialBackOff()
|
|
retryStrategy.MaxElapsedTime = 30 * time.Minute
|
|
err := backoff.Retry(
|
|
func() error {
|
|
if err := server.PostJSONWithTimeout(
|
|
context.Background(), webhookURL, &fleet.ActivityWebhookPayload{
|
|
Timestamp: timestamp,
|
|
ActorFullName: userName,
|
|
ActorID: userID,
|
|
ActorEmail: userEmail,
|
|
Type: activityType,
|
|
Details: (*json.RawMessage)(&detailsBytes),
|
|
},
|
|
); err != nil {
|
|
var statusCoder kithttp.StatusCoder
|
|
if errors.As(err, &statusCoder) && statusCoder.StatusCode() == http.StatusTooManyRequests {
|
|
level.Debug(a.logger).Log("msg", "fire activity webhook", "err", err)
|
|
return err
|
|
}
|
|
return backoff.Permanent(err)
|
|
}
|
|
return nil
|
|
}, retryStrategy,
|
|
)
|
|
if err != nil {
|
|
level.Error(a.logger).Log(
|
|
"msg", fmt.Sprintf("fire activity webhook to %s", server.MaskSecretURLParams(webhookURL)), "err",
|
|
server.MaskURLError(err).Error(),
|
|
)
|
|
}
|
|
}()
|
|
}
|
|
// We update the context to indicate that we processed the webhook.
|
|
ctx = context.WithValue(ctx, fleet.ActivityWebhookContextKey, true)
|
|
return a.repo.NewActivity(ctx, user, activity, detailsBytes, timestamp)
|
|
}
|