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:** For #39344 # Details As a first step to deprecating API params like `team_id` in favor of `fleet_id` and `query_id` in favor of `report_id`, this PR adds `renameto` tags to all deprecated keys. There is no logic in this PR to actually use these tags in any way. The logic and test fixes will be in the next PR, but in the interest of keeping things manageable I'm pushing this out first. There were definitely params with "query" in them that we don't want to change (mainly osquery-related), and I think I kept them all out but it's worth double-checking here. The team -> fleet changes are pretty safe in comparison. # Checklist for submitter If some of the following don't apply, delete the relevant line. - [ ] 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. Deferring changelog to PR with logic changes ## Testing - [ ] Added/updated automated tests This should be a no-op. All existing tests shoud pass. - [X] QA'd all new/changed functionality manually
614 lines
16 KiB
Go
614 lines
16 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/fleetdm/fleet/v4/server/authz"
|
|
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
|
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
)
|
|
|
|
type packResponse struct {
|
|
fleet.Pack
|
|
QueryCount uint `json:"query_count" renameto:"report_count"`
|
|
|
|
// All current hosts in the pack. Hosts which are selected explicty and
|
|
// hosts which are part of a label.
|
|
TotalHostsCount uint `json:"total_hosts_count"`
|
|
|
|
// IDs of hosts which were explicitly selected.
|
|
HostIDs []uint `json:"host_ids"`
|
|
LabelIDs []uint `json:"label_ids"`
|
|
TeamIDs []uint `json:"team_ids" renameto:"fleet_ids"`
|
|
}
|
|
|
|
func userIsGitOpsOnly(ctx context.Context) (bool, error) {
|
|
vc, ok := viewer.FromContext(ctx)
|
|
if !ok {
|
|
return false, fleet.ErrNoContext
|
|
}
|
|
if vc.User == nil {
|
|
return false, errors.New("missing user in context")
|
|
}
|
|
if vc.User.GlobalRole != nil {
|
|
return *vc.User.GlobalRole == fleet.RoleGitOps, nil
|
|
}
|
|
if len(vc.User.Teams) == 0 {
|
|
return false, errors.New("user has no roles")
|
|
}
|
|
for _, teamRole := range vc.User.Teams {
|
|
if teamRole.Role != fleet.RoleGitOps {
|
|
return false, nil
|
|
}
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func packResponseForPack(ctx context.Context, svc fleet.Service, pack fleet.Pack) (*packResponse, error) {
|
|
opts := fleet.ListOptions{}
|
|
queries, err := svc.GetScheduledQueriesInPack(ctx, pack.ID, opts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
totalHostsCount := uint(0)
|
|
|
|
hostMetrics, err := svc.CountHostsInTargets(
|
|
ctx,
|
|
nil,
|
|
fleet.HostTargets{
|
|
HostIDs: pack.HostIDs,
|
|
LabelIDs: pack.LabelIDs,
|
|
TeamIDs: pack.TeamIDs,
|
|
},
|
|
)
|
|
if err != nil {
|
|
var authErr *authz.Forbidden
|
|
if !errors.As(err, &authErr) {
|
|
return nil, err
|
|
}
|
|
// Some users (e.g. gitops) are not able to read targets, thus
|
|
// we do not fail when gathering the total host count to not fail
|
|
// write packs request.
|
|
ok, gerr := userIsGitOpsOnly(ctx)
|
|
if gerr != nil {
|
|
return nil, gerr
|
|
}
|
|
if !ok {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if hostMetrics != nil {
|
|
totalHostsCount = hostMetrics.TotalHosts
|
|
}
|
|
|
|
return &packResponse{
|
|
Pack: pack,
|
|
QueryCount: uint(len(queries)),
|
|
TotalHostsCount: totalHostsCount,
|
|
HostIDs: pack.HostIDs,
|
|
LabelIDs: pack.LabelIDs,
|
|
TeamIDs: pack.TeamIDs,
|
|
}, nil
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Get Pack
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type getPackRequest struct {
|
|
ID uint `url:"id"`
|
|
}
|
|
|
|
type getPackResponse struct {
|
|
Pack packResponse `json:"pack,omitempty"`
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r getPackResponse) Error() error { return r.Err }
|
|
|
|
func getPackEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*getPackRequest)
|
|
pack, err := svc.GetPack(ctx, req.ID)
|
|
if err != nil {
|
|
return getPackResponse{Err: err}, nil
|
|
}
|
|
|
|
resp, err := packResponseForPack(ctx, svc, *pack)
|
|
if err != nil {
|
|
return getPackResponse{Err: err}, nil
|
|
}
|
|
|
|
return getPackResponse{
|
|
Pack: *resp,
|
|
}, nil
|
|
}
|
|
|
|
func (svc *Service) GetPack(ctx context.Context, id uint) (*fleet.Pack, error) {
|
|
if err := svc.authz.Authorize(ctx, &fleet.Pack{}, fleet.ActionRead); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return svc.ds.Pack(ctx, id)
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Create Pack
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type createPackRequest struct {
|
|
fleet.PackPayload
|
|
}
|
|
|
|
type createPackResponse struct {
|
|
Pack packResponse `json:"pack,omitempty"`
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r createPackResponse) Error() error { return r.Err }
|
|
|
|
func createPackEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*createPackRequest)
|
|
pack, err := svc.NewPack(ctx, req.PackPayload)
|
|
if err != nil {
|
|
return createPackResponse{Err: err}, nil
|
|
}
|
|
|
|
resp, err := packResponseForPack(ctx, svc, *pack)
|
|
if err != nil {
|
|
return createPackResponse{Err: err}, nil
|
|
}
|
|
|
|
return createPackResponse{
|
|
Pack: *resp,
|
|
}, nil
|
|
}
|
|
|
|
func (svc *Service) NewPack(ctx context.Context, p fleet.PackPayload) (*fleet.Pack, error) {
|
|
if err := svc.authz.Authorize(ctx, &fleet.Pack{}, fleet.ActionWrite); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := p.Verify(); err != nil {
|
|
return nil, ctxerr.Wrap(ctx, &fleet.BadRequestError{
|
|
Message: fmt.Sprintf("pack payload verification: %s", err),
|
|
})
|
|
}
|
|
|
|
var pack fleet.Pack
|
|
|
|
if p.Name != nil {
|
|
pack.Name = *p.Name
|
|
}
|
|
|
|
if p.Description != nil {
|
|
pack.Description = *p.Description
|
|
}
|
|
|
|
if p.Platform != nil {
|
|
pack.Platform = *p.Platform
|
|
}
|
|
|
|
if p.Disabled != nil {
|
|
pack.Disabled = *p.Disabled
|
|
}
|
|
|
|
if p.HostIDs != nil {
|
|
pack.HostIDs = *p.HostIDs
|
|
}
|
|
|
|
if p.LabelIDs != nil {
|
|
pack.LabelIDs = *p.LabelIDs
|
|
}
|
|
|
|
if p.TeamIDs != nil {
|
|
pack.TeamIDs = *p.TeamIDs
|
|
}
|
|
|
|
_, err := svc.ds.NewPack(ctx, &pack)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := svc.NewActivity(
|
|
ctx,
|
|
authz.UserFromContext(ctx),
|
|
fleet.ActivityTypeCreatedPack{
|
|
ID: pack.ID,
|
|
Name: pack.Name,
|
|
},
|
|
); err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "create activity for pack creation")
|
|
}
|
|
|
|
return &pack, nil
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Modify Pack
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type modifyPackRequest struct {
|
|
ID uint `json:"-" url:"id"`
|
|
fleet.PackPayload
|
|
}
|
|
|
|
type modifyPackResponse struct {
|
|
Pack packResponse `json:"pack,omitempty"`
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r modifyPackResponse) Error() error { return r.Err }
|
|
|
|
func modifyPackEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*modifyPackRequest)
|
|
pack, err := svc.ModifyPack(ctx, req.ID, req.PackPayload)
|
|
if err != nil {
|
|
return modifyPackResponse{Err: err}, nil
|
|
}
|
|
|
|
resp, err := packResponseForPack(ctx, svc, *pack)
|
|
if err != nil {
|
|
return modifyPackResponse{Err: err}, nil
|
|
}
|
|
|
|
return modifyPackResponse{
|
|
Pack: *resp,
|
|
}, nil
|
|
}
|
|
|
|
func (svc *Service) ModifyPack(ctx context.Context, id uint, p fleet.PackPayload) (*fleet.Pack, error) {
|
|
if err := svc.authz.Authorize(ctx, &fleet.Pack{}, fleet.ActionWrite); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := p.Verify(); err != nil {
|
|
return nil, ctxerr.Wrap(ctx, &fleet.BadRequestError{
|
|
Message: fmt.Sprintf("pack payload verification: %s", err),
|
|
})
|
|
}
|
|
|
|
pack, err := svc.ds.Pack(ctx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if p.Name != nil && pack.EditablePackType() {
|
|
pack.Name = *p.Name
|
|
}
|
|
|
|
if p.Description != nil && pack.EditablePackType() {
|
|
pack.Description = *p.Description
|
|
}
|
|
|
|
if p.Platform != nil {
|
|
pack.Platform = *p.Platform
|
|
}
|
|
|
|
if p.Disabled != nil {
|
|
pack.Disabled = *p.Disabled
|
|
}
|
|
|
|
if p.HostIDs != nil && pack.EditablePackType() {
|
|
pack.HostIDs = *p.HostIDs
|
|
}
|
|
|
|
if p.LabelIDs != nil && pack.EditablePackType() {
|
|
pack.LabelIDs = *p.LabelIDs
|
|
}
|
|
|
|
if p.TeamIDs != nil && pack.EditablePackType() {
|
|
pack.TeamIDs = *p.TeamIDs
|
|
}
|
|
|
|
err = svc.ds.SavePack(ctx, pack)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := svc.NewActivity(
|
|
ctx,
|
|
authz.UserFromContext(ctx),
|
|
fleet.ActivityTypeEditedPack{
|
|
ID: pack.ID,
|
|
Name: pack.Name,
|
|
},
|
|
); err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "create activity for pack modification")
|
|
}
|
|
|
|
return pack, err
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// List Packs
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type listPacksRequest struct {
|
|
ListOptions fleet.ListOptions `url:"list_options"`
|
|
}
|
|
|
|
type listPacksResponse struct {
|
|
Packs []packResponse `json:"packs"`
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r listPacksResponse) Error() error { return r.Err }
|
|
|
|
func listPacksEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*listPacksRequest)
|
|
packs, err := svc.ListPacks(ctx, fleet.PackListOptions{ListOptions: req.ListOptions, IncludeSystemPacks: false})
|
|
if err != nil {
|
|
return getPackResponse{Err: err}, nil
|
|
}
|
|
|
|
resp := listPacksResponse{Packs: make([]packResponse, len(packs))}
|
|
for i, pack := range packs {
|
|
packResp, err := packResponseForPack(ctx, svc, *pack)
|
|
if err != nil {
|
|
return getPackResponse{Err: err}, nil
|
|
}
|
|
resp.Packs[i] = *packResp
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
func (svc *Service) ListPacks(ctx context.Context, opt fleet.PackListOptions) ([]*fleet.Pack, error) {
|
|
if err := svc.authz.Authorize(ctx, &fleet.Pack{}, fleet.ActionRead); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return svc.ds.ListPacks(ctx, opt)
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Delete Pack
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type deletePackRequest struct {
|
|
Name string `url:"name"`
|
|
}
|
|
|
|
type deletePackResponse struct {
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r deletePackResponse) Error() error { return r.Err }
|
|
|
|
func deletePackEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*deletePackRequest)
|
|
err := svc.DeletePack(ctx, req.Name)
|
|
if err != nil {
|
|
return deletePackResponse{Err: err}, nil
|
|
}
|
|
return deletePackResponse{}, nil
|
|
}
|
|
|
|
func (svc *Service) DeletePack(ctx context.Context, name string) error {
|
|
if err := svc.authz.Authorize(ctx, &fleet.Pack{}, fleet.ActionWrite); err != nil {
|
|
return err
|
|
}
|
|
|
|
pack, _, err := svc.ds.PackByName(ctx, name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// if there is a pack by this name, ensure it is not type Global or Team
|
|
if pack != nil && !pack.EditablePackType() {
|
|
return fmt.Errorf("cannot delete pack_type %s", *pack.Type)
|
|
}
|
|
|
|
if err := svc.ds.DeletePack(ctx, name); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := svc.NewActivity(
|
|
ctx,
|
|
authz.UserFromContext(ctx),
|
|
fleet.ActivityTypeDeletedPack{
|
|
Name: pack.Name,
|
|
},
|
|
); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "create activity for pack deletion")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Delete Pack By ID
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type deletePackByIDRequest struct {
|
|
ID uint `url:"id"`
|
|
}
|
|
|
|
type deletePackByIDResponse struct {
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r deletePackByIDResponse) Error() error { return r.Err }
|
|
|
|
func deletePackByIDEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*deletePackByIDRequest)
|
|
err := svc.DeletePackByID(ctx, req.ID)
|
|
if err != nil {
|
|
return deletePackByIDResponse{Err: err}, nil
|
|
}
|
|
return deletePackByIDResponse{}, nil
|
|
}
|
|
|
|
func (svc *Service) DeletePackByID(ctx context.Context, id uint) error {
|
|
if err := svc.authz.Authorize(ctx, &fleet.Pack{}, fleet.ActionWrite); err != nil {
|
|
return err
|
|
}
|
|
|
|
pack, err := svc.ds.Pack(ctx, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if pack != nil && !pack.EditablePackType() {
|
|
return fmt.Errorf("cannot delete pack_type %s", *pack.Type)
|
|
}
|
|
if err := svc.ds.DeletePack(ctx, pack.Name); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := svc.NewActivity(
|
|
ctx,
|
|
authz.UserFromContext(ctx),
|
|
fleet.ActivityTypeDeletedPack{
|
|
Name: pack.Name,
|
|
},
|
|
); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "create activity for pack deletion by id")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Apply Pack Spec
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type applyPackSpecsRequest struct {
|
|
Specs []*fleet.PackSpec `json:"specs"`
|
|
}
|
|
|
|
type applyPackSpecsResponse struct {
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r applyPackSpecsResponse) Error() error { return r.Err }
|
|
|
|
func applyPackSpecsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*applyPackSpecsRequest)
|
|
_, err := svc.ApplyPackSpecs(ctx, req.Specs)
|
|
if err != nil {
|
|
return applyPackSpecsResponse{Err: err}, nil
|
|
}
|
|
return applyPackSpecsResponse{}, nil
|
|
}
|
|
|
|
func (svc *Service) ApplyPackSpecs(ctx context.Context, specs []*fleet.PackSpec) ([]*fleet.PackSpec, error) {
|
|
if err := svc.authz.Authorize(ctx, &fleet.Pack{}, fleet.ActionWrite); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
packs, err := svc.ds.ListPacks(ctx, fleet.PackListOptions{IncludeSystemPacks: true})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
namePacks := make(map[string]*fleet.Pack, len(packs))
|
|
for _, pack := range packs {
|
|
namePacks[pack.Name] = pack
|
|
}
|
|
|
|
var result []*fleet.PackSpec
|
|
|
|
// loop over incoming specs filtering out possible edits to Global or Team Packs
|
|
for _, spec := range specs {
|
|
// see for known limitations https://github.com/fleetdm/fleet/pull/1558#discussion_r684218301
|
|
// check to see if incoming spec is already in the list of packs
|
|
if p, ok := namePacks[spec.Name]; ok {
|
|
// as long as pack is editable, we'll apply it
|
|
if p.EditablePackType() {
|
|
result = append(result, spec)
|
|
}
|
|
} else {
|
|
// incoming spec is new, let's apply it
|
|
result = append(result, spec)
|
|
}
|
|
}
|
|
|
|
for _, packSpec := range result {
|
|
if err := packSpec.Verify(); err != nil {
|
|
return nil, ctxerr.Wrap(ctx, &fleet.BadRequestError{
|
|
Message: fmt.Sprintf("pack payload verification: %s", err),
|
|
})
|
|
}
|
|
}
|
|
|
|
if err := svc.ds.ApplyPackSpecs(ctx, result); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := svc.NewActivity(
|
|
ctx,
|
|
authz.UserFromContext(ctx),
|
|
fleet.ActivityTypeAppliedSpecPack{},
|
|
); err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "create activity for pack spec")
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Get Pack Specs
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type getPackSpecsResponse struct {
|
|
Specs []*fleet.PackSpec `json:"specs"`
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r getPackSpecsResponse) Error() error { return r.Err }
|
|
|
|
func getPackSpecsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
specs, err := svc.GetPackSpecs(ctx)
|
|
if err != nil {
|
|
return getPackSpecsResponse{Err: err}, nil
|
|
}
|
|
return getPackSpecsResponse{Specs: specs}, nil
|
|
}
|
|
|
|
func (svc *Service) GetPackSpecs(ctx context.Context) ([]*fleet.PackSpec, error) {
|
|
if err := svc.authz.Authorize(ctx, &fleet.Pack{}, fleet.ActionRead); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return svc.ds.GetPackSpecs(ctx)
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Get Pack Spec
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type getPackSpecResponse struct {
|
|
Spec *fleet.PackSpec `json:"specs,omitempty"`
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r getPackSpecResponse) Error() error { return r.Err }
|
|
|
|
func getPackSpecEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*getGenericSpecRequest)
|
|
spec, err := svc.GetPackSpec(ctx, req.Name)
|
|
if err != nil {
|
|
return getPackSpecResponse{Err: err}, nil
|
|
}
|
|
return getPackSpecResponse{Spec: spec}, nil
|
|
}
|
|
|
|
func (svc *Service) GetPackSpec(ctx context.Context, name string) (*fleet.PackSpec, error) {
|
|
if err := svc.authz.Authorize(ctx, &fleet.Pack{}, fleet.ActionRead); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return svc.ds.GetPackSpec(ctx, name)
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// List Packs For Host, not exposed via an endpoint
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
func (svc *Service) ListPacksForHost(ctx context.Context, hid uint) ([]*fleet.Pack, error) {
|
|
if err := svc.authz.Authorize(ctx, &fleet.Pack{}, fleet.ActionRead); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return svc.ds.ListPacksForHost(ctx, hid)
|
|
}
|