fleet/server/service/packs.go
Lucas Manuel Rodriguez 87709d8c95
Fix permissions on GitOps user for searching hosts or count targets (#11448)
#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)).~
2023-05-01 12:57:28 -03:00

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)
}