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 #39303 (child of #25080). - Added `inherited_query_count` to `ListQueriesResponse` (thought of adding a brand new endpoint just for counting, but felt like extending the current one was good enough). In the parent task, [it was suggested](https://github.com/fleetdm/fleet/issues/25080#issuecomment-3326071574) to `"Depend on team list entity endpoint's count field / team entity count endpoint for whether or not to disable the manage automations button"`, which Rachael approved, so I went for this approach. - The `ManageQueryAutomationsModal` now fetches its own data with `merge_inherited = false` (meaning it only fetches non-inherited queries only). Previously, queries were passed down as props to it, which would not show the queries available to automate if the first page of queries were all inherited and the second page contained queries for that team (the user would have to navigate to the second page for the button to be enabled). ^ The fact that the modal fetches its own data is similar behavior to what is currently done in `Policies`. For queries, I noticed that we would need to add pagination within the `Manage Automations` modal, but that can be a follow-up. <img width="2480" height="1309" alt="Screenshot 2026-02-04 at 11 48 42 AM" src="https://github.com/user-attachments/assets/ebac79a5-a793-4708-9313-d9a697dfd7de" /> # 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] QA'd all new/changed functionality manually https://github.com/user-attachments/assets/119f03b9-dde1-4bb9-9fee-6204b1a58879
200 lines
6.6 KiB
Go
200 lines
6.6 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
"github.com/fleetdm/fleet/v4/server/ptr"
|
|
"gopkg.in/guregu/null.v3"
|
|
)
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
// Get Scheduled Queries of a team.
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type getTeamScheduleRequest struct {
|
|
TeamID uint `url:"team_id"`
|
|
ListOptions fleet.ListOptions `url:"list_options"`
|
|
}
|
|
|
|
type getTeamScheduleResponse struct {
|
|
Scheduled []scheduledQueryResponse `json:"scheduled"`
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r getTeamScheduleResponse) Error() error { return r.Err }
|
|
|
|
func getTeamScheduleEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*getTeamScheduleRequest)
|
|
resp := getTeamScheduleResponse{Scheduled: []scheduledQueryResponse{}}
|
|
queries, err := svc.GetTeamScheduledQueries(ctx, req.TeamID, req.ListOptions)
|
|
if err != nil {
|
|
return getTeamScheduleResponse{Err: err}, nil
|
|
}
|
|
for _, q := range queries {
|
|
resp.Scheduled = append(resp.Scheduled, scheduledQueryResponse{
|
|
ScheduledQuery: *q,
|
|
})
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
func (svc Service) GetTeamScheduledQueries(ctx context.Context, teamID uint, opts fleet.ListOptions) ([]*fleet.ScheduledQuery, error) {
|
|
var teamID_ *uint
|
|
if teamID != 0 {
|
|
teamID_ = &teamID
|
|
}
|
|
queries, _, _, _, err := svc.ListQueries(ctx, opts, teamID_, ptr.Bool(true), false, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
scheduledQueries := make([]*fleet.ScheduledQuery, 0, len(queries))
|
|
for _, query := range queries {
|
|
scheduledQueries = append(scheduledQueries, fleet.ScheduledQueryFromQuery(query))
|
|
}
|
|
return scheduledQueries, nil
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
// Add schedule query to a team.
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type teamScheduleQueryRequest struct {
|
|
TeamID uint `url:"team_id"`
|
|
fleet.ScheduledQueryPayload
|
|
}
|
|
|
|
type teamScheduleQueryResponse struct {
|
|
Scheduled *fleet.ScheduledQuery `json:"scheduled,omitempty"`
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r teamScheduleQueryResponse) Error() error { return r.Err }
|
|
|
|
func uintValueOrZero(v *uint) uint {
|
|
if v == nil {
|
|
return 0
|
|
}
|
|
return *v
|
|
}
|
|
|
|
func nullIntToPtrUint(v *null.Int) *uint {
|
|
if v == nil {
|
|
return nil
|
|
}
|
|
return ptr.Uint(uint(v.ValueOrZero())) //nolint:gosec // dismiss G115
|
|
}
|
|
|
|
func teamScheduleQueryEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*teamScheduleQueryRequest)
|
|
resp, err := svc.TeamScheduleQuery(ctx, req.TeamID, &fleet.ScheduledQuery{
|
|
QueryID: uintValueOrZero(req.QueryID),
|
|
Interval: uintValueOrZero(req.Interval),
|
|
Snapshot: req.Snapshot,
|
|
Removed: req.Removed,
|
|
Platform: req.Platform,
|
|
Version: req.Version,
|
|
Shard: nullIntToPtrUint(req.Shard),
|
|
})
|
|
if err != nil {
|
|
return teamScheduleQueryResponse{Err: err}, nil
|
|
}
|
|
return teamScheduleQueryResponse{
|
|
Scheduled: resp,
|
|
}, nil
|
|
}
|
|
|
|
func nameForCopiedQuery(originalName string) string {
|
|
return "Copy of " + originalName + " (" + fmt.Sprintf("%d", time.Now().Unix()) + ")"
|
|
}
|
|
|
|
func (svc Service) TeamScheduleQuery(ctx context.Context, teamID uint, scheduledQuery *fleet.ScheduledQuery) (*fleet.ScheduledQuery, error) {
|
|
originalQuery, err := svc.ds.Query(ctx, scheduledQuery.QueryID)
|
|
if err != nil {
|
|
setAuthCheckedOnPreAuthErr(ctx)
|
|
return nil, ctxerr.Wrap(ctx, err, "get query from id")
|
|
}
|
|
if originalQuery.TeamID != nil {
|
|
setAuthCheckedOnPreAuthErr(ctx)
|
|
return nil, ctxerr.New(ctx, "cannot create a team schedule from a team query")
|
|
}
|
|
originalQuery.Name = nameForCopiedQuery(originalQuery.Name)
|
|
originalQuery.TeamID = &teamID
|
|
newQuery, err := svc.NewQuery(ctx, fleet.ScheduledQueryToQueryPayloadForNewQuery(originalQuery, scheduledQuery))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return fleet.ScheduledQueryFromQuery(newQuery), nil
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
// Modify team scheduled query.
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type modifyTeamScheduleRequest struct {
|
|
TeamID uint `url:"team_id"`
|
|
ScheduledQueryID uint `url:"scheduled_query_id"`
|
|
fleet.ScheduledQueryPayload
|
|
}
|
|
|
|
type modifyTeamScheduleResponse struct {
|
|
Scheduled *fleet.ScheduledQuery `json:"scheduled,omitempty"`
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r modifyTeamScheduleResponse) Error() error { return r.Err }
|
|
|
|
func modifyTeamScheduleEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*modifyTeamScheduleRequest)
|
|
if _, err := svc.ModifyTeamScheduledQueries(ctx, req.TeamID, req.ScheduledQueryID, req.ScheduledQueryPayload); err != nil {
|
|
return modifyTeamScheduleResponse{Err: err}, nil
|
|
}
|
|
return modifyTeamScheduleResponse{}, nil
|
|
}
|
|
|
|
// teamID is not used because of mismatch between old internal representation and API.
|
|
func (svc Service) ModifyTeamScheduledQueries(
|
|
ctx context.Context,
|
|
teamID uint,
|
|
scheduledQueryID uint,
|
|
scheduledQueryPayload fleet.ScheduledQueryPayload,
|
|
) (*fleet.ScheduledQuery, error) {
|
|
query, err := svc.ModifyQuery(ctx, scheduledQueryID, fleet.ScheduledQueryPayloadToQueryPayloadForModifyQuery(scheduledQueryPayload))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return fleet.ScheduledQueryFromQuery(query), nil
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
// Delete a scheduled query from a team.
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type deleteTeamScheduleRequest struct {
|
|
TeamID uint `url:"team_id"`
|
|
ScheduledQueryID uint `url:"scheduled_query_id"`
|
|
}
|
|
|
|
type deleteTeamScheduleResponse struct {
|
|
Scheduled *fleet.ScheduledQuery `json:"scheduled,omitempty"`
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r deleteTeamScheduleResponse) Error() error { return r.Err }
|
|
|
|
func deleteTeamScheduleEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*deleteTeamScheduleRequest)
|
|
err := svc.DeleteTeamScheduledQueries(ctx, req.TeamID, req.ScheduledQueryID)
|
|
if err != nil {
|
|
return deleteTeamScheduleResponse{Err: err}, nil
|
|
}
|
|
return deleteTeamScheduleResponse{}, nil
|
|
}
|
|
|
|
// teamID is not used because of mismatch between old internal representation and API.
|
|
func (svc Service) DeleteTeamScheduledQueries(ctx context.Context, teamID uint, scheduledQueryID uint) error {
|
|
return svc.DeleteQueryByID(ctx, scheduledQueryID)
|
|
}
|