mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 00:45:19 +00:00
87709d8c95
#11447 - ~[ ] Changes file added for user-visible changes in `changes/` or `orbit/changes/`. See [Changes files](https://fleetdm.com/docs/contributing/committing-changes#changes-files) for more information.~ - ~[ ] Documented any API changes (docs/Using-Fleet/REST-API.md or docs/Contributing/API-for-contributors.md)~ - ~[ ] Documented any permissions changes~ - ~[ ] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements)~ - ~[ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for new osquery data ingestion features.~ - [X] Added/updated tests - [X] Manual QA for all new/changed functionality - ~For Orbit and Fleet Desktop changes:~ - ~[ ] Manual QA must be performed in the three main OSs, macOS, Windows and Linux.~ - ~[ ] Auto-update manual QA, from released version of component to new version (see [tools/tuf/test](../tools/tuf/test/README.md)).~
615 lines
16 KiB
Go
615 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"`
|
|
|
|
// 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"`
|
|
}
|
|
|
|
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) (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) (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.ds.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) (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.ds.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) (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) (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.ds.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) (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.ds.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) (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.ds.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) (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) (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)
|
|
}
|