Files
Nico 5a1365dc41 40493 webhooks for host activities (#50595)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #40493

Changes already reviewed in the PRs merged to this feature branch.
Only additive change was
https://github.com/fleetdm/fleet/pull/50595/commits/c0934e1fee46a734f9499a4c782563d4fcc345c4
to address CodeRabbit's comments.

# Checklist for submitter

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.

## Testing

- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually



https://github.com/user-attachments/assets/ea7f5157-a67a-4d83-842d-62197bd1546d



## New Fleet configuration settings

- [ ] Setting(s) is/are explicitly excluded from GitOps

If you didn't check the box above, follow this checklist for
GitOps-enabled settings:

- [x] Verified that the setting is exported via `fleetctl
generate-gitops`
- [x] Verified the setting is documented in a separate PR to [the GitOps
documentation](https://github.com/fleetdm/fleet/blob/main/docs/Configuration/yaml-files.md#L485)
- [x] Verified that the setting is cleared on the server if it is not
supplied in a YAML file (or that it is documented as being optional)
- [x] Verified that any relevant UI is disabled when GitOps mode is
enabled

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Summary by CodeRabbit

* **New Features**
* Added host activity automations with configurable webhook
destinations.
* Manage automations from the Hosts page with validation, permissions,
and enable/disable controls.
  * Added GitOps support for team and unassigned-host webhook settings.
* Activity webhooks now include fleet-scoped host IDs where applicable.
  * Added profile UUIDs to MDM profile resend activity details.

* **Bug Fixes**
* Improved Windows MDM enrollment activity details by including the
linked host ID when available.
  * Preserved existing webhook settings when omitted during updates.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-07 09:49:24 -03:00

148 lines
5.0 KiB
Go

// Package activityacl provides the anti-corruption layer between the activity
// bounded context and legacy Fleet code.
//
// This package is the ONLY place that imports both activity types and fleet types.
// It translates between them, allowing the activity context to remain decoupled
// from legacy code.
package activityacl
import (
"context"
"github.com/fleetdm/fleet/v4/server/activity"
"github.com/fleetdm/fleet/v4/server/fleet"
)
// UsersByIDsLookup is the minimal interface needed to look up user summaries by ID
// without going through the service layer's authz. The activity bounded context
// performs its own authorization on the host before reaching the user-enrichment
// step, so a second authz check (which would reject team-scoped viewers since
// Service.UsersByIDs authorizes against an empty *fleet.User) is both incorrect
// and the cause of issue #46009 for hosts with user-initiated past activities.
type UsersByIDsLookup interface {
UsersByIDs(ctx context.Context, ids []uint) ([]*fleet.UserSummary, error)
}
// FleetServiceAdapter provides access to Fleet service methods
// for data that the activity bounded context doesn't own.
type FleetServiceAdapter struct {
svc fleet.ActivityLookupService
usersByIDsDS UsersByIDsLookup
}
// NewFleetServiceAdapter creates a new adapter for the Fleet service.
// usersByIDsDS is a datastore-direct lookup used for activity user enrichment;
// it must bypass service-layer authz because the host authz check has already
// gated access to the activity list.
func NewFleetServiceAdapter(svc fleet.ActivityLookupService, usersByIDsDS UsersByIDsLookup) *FleetServiceAdapter {
return &FleetServiceAdapter{svc: svc, usersByIDsDS: usersByIDsDS}
}
// Ensure FleetServiceAdapter implements the required interfaces
var (
_ activity.UserProvider = (*FleetServiceAdapter)(nil)
_ activity.HostProvider = (*FleetServiceAdapter)(nil)
_ activity.AppConfigProvider = (*FleetServiceAdapter)(nil)
_ activity.UpcomingActivityActivator = (*FleetServiceAdapter)(nil)
)
// UsersByIDs fetches users by their IDs from the datastore directly,
// bypassing service-layer authz. The activity service has already authorized
// the caller against the host before this is called for enrichment.
func (a *FleetServiceAdapter) UsersByIDs(ctx context.Context, ids []uint) ([]*activity.User, error) {
if len(ids) == 0 {
return nil, nil
}
// Fetch only the requested users by their IDs
users, err := a.usersByIDsDS.UsersByIDs(ctx, ids)
if err != nil {
return nil, err
}
// Convert to activity.User
result := make([]*activity.User, 0, len(users))
for _, u := range users {
result = append(result, convertUser(u))
}
return result, nil
}
// FindUserIDs searches for users by name/email prefix and returns their IDs.
func (a *FleetServiceAdapter) FindUserIDs(ctx context.Context, query string) ([]uint, error) {
if query == "" {
return nil, nil
}
// Search users via Fleet service with the query
users, err := a.svc.ListUsers(ctx, fleet.UserListOptions{
ListOptions: fleet.ListOptions{
MatchQuery: query,
},
})
if err != nil {
return nil, err
}
ids := make([]uint, 0, len(users))
for _, u := range users {
ids = append(ids, u.ID)
}
return ids, nil
}
// GetHostLite fetches minimal host information for authorization.
func (a *FleetServiceAdapter) GetHostLite(ctx context.Context, hostID uint) (*activity.Host, error) {
host, err := a.svc.GetHostLite(ctx, hostID)
if err != nil {
return nil, err
}
return &activity.Host{
ID: host.ID,
TeamID: host.TeamID,
}, nil
}
func convertUser(u *fleet.UserSummary) *activity.User {
return &activity.User{
ID: u.ID,
Name: u.Name,
Email: u.Email,
Gravatar: u.GravatarURL,
APIOnly: u.APIOnly,
}
}
// GetActivitiesWebhookConfig returns the webhook configuration for activities.
func (a *FleetServiceAdapter) GetActivitiesWebhookConfig(ctx context.Context) (*activity.ActivitiesWebhookSettings, error) {
settings, err := a.svc.GetActivitiesWebhookSettings(ctx)
if err != nil {
return nil, err
}
return &activity.ActivitiesWebhookSettings{
Enable: settings.Enable,
DestinationURL: settings.DestinationURL,
}, nil
}
// GetHostActivitiesWebhooks returns the enabled host-activities webhook destinations of the fleets the given hosts belong to.
func (a *FleetServiceAdapter) GetHostActivitiesWebhooks(ctx context.Context, hostIDs []uint) ([]activity.HostActivitiesWebhook, error) {
settings, err := a.svc.GetHostActivitiesWebhookSettings(ctx, hostIDs)
if err != nil {
return nil, err
}
hooks := make([]activity.HostActivitiesWebhook, 0, len(settings))
for _, s := range settings {
hooks = append(hooks, activity.HostActivitiesWebhook{
DestinationURL: s.DestinationURL,
HostIDs: s.HostIDs,
})
}
return hooks, nil
}
// ActivateNextUpcomingActivity activates the next upcoming activity in the queue.
func (a *FleetServiceAdapter) ActivateNextUpcomingActivity(ctx context.Context, hostID uint, fromCompletedExecID string) error {
return a.svc.ActivateNextUpcomingActivityForHost(ctx, hostID, fromCompletedExecID)
}