2021-09-29 16:13:23 +00:00
|
|
|
package service
|
|
|
|
|
|
|
|
import (
|
2022-05-10 18:25:53 +00:00
|
|
|
"bytes"
|
2021-09-29 16:13:23 +00:00
|
|
|
"context"
|
2022-05-10 18:25:53 +00:00
|
|
|
"encoding/csv"
|
2022-05-23 19:35:05 +00:00
|
|
|
"encoding/json"
|
2022-03-15 19:14:42 +00:00
|
|
|
"fmt"
|
2022-05-10 18:25:53 +00:00
|
|
|
"io"
|
2022-03-15 19:14:42 +00:00
|
|
|
"net/http"
|
2022-05-10 18:25:53 +00:00
|
|
|
"strings"
|
2021-12-14 21:34:11 +00:00
|
|
|
"time"
|
2021-09-29 16:13:23 +00:00
|
|
|
|
2023-02-08 23:20:23 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/server/authz"
|
|
|
|
authzctx "github.com/fleetdm/fleet/v4/server/contexts/authz"
|
2021-11-22 14:13:26 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
2022-11-15 14:08:05 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/server/contexts/license"
|
2022-03-15 19:14:42 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/server/contexts/logging"
|
2021-10-11 14:37:48 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
|
2021-09-29 16:13:23 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
2023-02-08 23:20:23 +00:00
|
|
|
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
|
2022-03-15 19:14:42 +00:00
|
|
|
"github.com/gocarina/gocsv"
|
2021-09-29 16:13:23 +00:00
|
|
|
)
|
|
|
|
|
2021-12-14 21:34:11 +00:00
|
|
|
// HostDetailResponse is the response struct that contains the full host information
|
|
|
|
// with the HostDetail details.
|
|
|
|
type HostDetailResponse struct {
|
|
|
|
fleet.HostDetail
|
2022-03-21 16:29:52 +00:00
|
|
|
Status fleet.HostStatus `json:"status"`
|
|
|
|
DisplayText string `json:"display_text"`
|
2022-10-11 21:00:32 +00:00
|
|
|
DisplayName string `json:"display_name"`
|
2022-03-21 16:29:52 +00:00
|
|
|
Geolocation *fleet.GeoLocation `json:"geolocation,omitempty"`
|
2021-12-14 21:34:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func hostDetailResponseForHost(ctx context.Context, svc fleet.Service, host *fleet.HostDetail) (*HostDetailResponse, error) {
|
|
|
|
return &HostDetailResponse{
|
|
|
|
HostDetail: *host,
|
|
|
|
Status: host.Status(time.Now()),
|
|
|
|
DisplayText: host.Hostname,
|
2022-10-11 21:00:32 +00:00
|
|
|
DisplayName: host.DisplayName(),
|
2022-03-21 16:29:52 +00:00
|
|
|
Geolocation: svc.LookupGeoIP(ctx, host.PublicIP),
|
2021-12-14 21:34:11 +00:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2021-10-11 14:37:48 +00:00
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// List Hosts
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
type listHostsRequest struct {
|
|
|
|
Opts fleet.HostListOptions `url:"host_options"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type listHostsResponse struct {
|
2022-10-14 14:14:18 +00:00
|
|
|
Hosts []fleet.HostResponse `json:"hosts"`
|
|
|
|
Software *fleet.Software `json:"software,omitempty"`
|
2022-08-15 16:57:25 +00:00
|
|
|
// MDMSolution is populated with the MDM solution corresponding to the mdm_id
|
|
|
|
// filter if one is provided with the request (and it exists in the
|
|
|
|
// database). It is nil otherwise and absent of the JSON response payload.
|
2022-09-06 14:34:06 +00:00
|
|
|
MDMSolution *fleet.MDMSolution `json:"mobile_device_management_solution,omitempty"`
|
2022-08-29 18:40:16 +00:00
|
|
|
// MunkiIssue is populated with the munki issue corresponding to the
|
|
|
|
// munki_issue_id filter if one is provided with the request (and it exists
|
|
|
|
// in the database). It is nil otherwise and absent of the JSON response
|
|
|
|
// payload.
|
2022-09-06 14:34:06 +00:00
|
|
|
MunkiIssue *fleet.MunkiIssue `json:"munki_issue,omitempty"`
|
2022-08-29 18:40:16 +00:00
|
|
|
|
|
|
|
Err error `json:"error,omitempty"`
|
2021-10-11 14:37:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (r listHostsResponse) error() error { return r.Err }
|
|
|
|
|
2022-12-27 14:26:59 +00:00
|
|
|
func listHostsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
2021-10-11 14:37:48 +00:00
|
|
|
req := request.(*listHostsRequest)
|
|
|
|
|
2021-10-12 18:59:01 +00:00
|
|
|
var software *fleet.Software
|
|
|
|
if req.Opts.SoftwareIDFilter != nil {
|
2022-05-20 16:58:40 +00:00
|
|
|
var err error
|
|
|
|
software, err = svc.SoftwareByID(ctx, *req.Opts.SoftwareIDFilter, false)
|
2021-10-12 18:59:01 +00:00
|
|
|
if err != nil {
|
|
|
|
return listHostsResponse{Err: err}, nil
|
|
|
|
}
|
|
|
|
}
|
2022-05-20 16:58:40 +00:00
|
|
|
|
2022-09-06 14:34:06 +00:00
|
|
|
var mdmSolution *fleet.MDMSolution
|
2022-08-15 16:57:25 +00:00
|
|
|
if req.Opts.MDMIDFilter != nil {
|
|
|
|
var err error
|
2022-09-06 14:34:06 +00:00
|
|
|
mdmSolution, err = svc.GetMDMSolution(ctx, *req.Opts.MDMIDFilter)
|
|
|
|
if err != nil && !fleet.IsNotFound(err) { // ignore not found, just return nil for the MDM solution in that case
|
2022-08-15 16:57:25 +00:00
|
|
|
return listHostsResponse{Err: err}, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-06 14:34:06 +00:00
|
|
|
var munkiIssue *fleet.MunkiIssue
|
2022-08-29 18:40:16 +00:00
|
|
|
if req.Opts.MunkiIssueIDFilter != nil {
|
|
|
|
var err error
|
2022-09-06 14:34:06 +00:00
|
|
|
munkiIssue, err = svc.GetMunkiIssue(ctx, *req.Opts.MunkiIssueIDFilter)
|
|
|
|
if err != nil && !fleet.IsNotFound(err) { // ignore not found, just return nil for the munki issue in that case
|
2022-08-29 18:40:16 +00:00
|
|
|
return listHostsResponse{Err: err}, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-20 16:58:40 +00:00
|
|
|
hosts, err := svc.ListHosts(ctx, req.Opts)
|
|
|
|
if err != nil {
|
|
|
|
return listHostsResponse{Err: err}, nil
|
|
|
|
}
|
|
|
|
|
2022-10-14 14:14:18 +00:00
|
|
|
hostResponses := make([]fleet.HostResponse, len(hosts))
|
2021-10-11 14:37:48 +00:00
|
|
|
for i, host := range hosts {
|
2022-10-14 14:14:18 +00:00
|
|
|
h, err := fleet.HostResponseForHost(ctx, svc, host)
|
2021-10-11 14:37:48 +00:00
|
|
|
if err != nil {
|
|
|
|
return listHostsResponse{Err: err}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
hostResponses[i] = *h
|
|
|
|
}
|
2022-08-29 18:40:16 +00:00
|
|
|
return listHostsResponse{
|
|
|
|
Hosts: hostResponses,
|
|
|
|
Software: software,
|
|
|
|
MDMSolution: mdmSolution,
|
|
|
|
MunkiIssue: munkiIssue,
|
|
|
|
}, nil
|
2022-08-15 16:57:25 +00:00
|
|
|
}
|
|
|
|
|
2022-09-06 14:34:06 +00:00
|
|
|
func (svc *Service) GetMDMSolution(ctx context.Context, mdmID uint) (*fleet.MDMSolution, error) {
|
|
|
|
// require list hosts permission to view this information
|
|
|
|
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
|
2022-08-15 16:57:25 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
2022-09-06 14:34:06 +00:00
|
|
|
return svc.ds.GetMDMSolution(ctx, mdmID)
|
2021-10-11 14:37:48 +00:00
|
|
|
}
|
|
|
|
|
2022-09-06 14:34:06 +00:00
|
|
|
func (svc *Service) GetMunkiIssue(ctx context.Context, munkiIssueID uint) (*fleet.MunkiIssue, error) {
|
|
|
|
// require list hosts permission to view this information
|
|
|
|
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
|
2022-08-29 18:40:16 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
2022-09-06 14:34:06 +00:00
|
|
|
return svc.ds.GetMunkiIssue(ctx, munkiIssueID)
|
2022-08-29 18:40:16 +00:00
|
|
|
}
|
|
|
|
|
2021-12-14 21:34:11 +00:00
|
|
|
func (svc *Service) ListHosts(ctx context.Context, opt fleet.HostListOptions) ([]*fleet.Host, error) {
|
2021-10-11 14:37:48 +00:00
|
|
|
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
vc, ok := viewer.FromContext(ctx)
|
|
|
|
if !ok {
|
|
|
|
return nil, fleet.ErrNoContext
|
|
|
|
}
|
|
|
|
filter := fleet.TeamFilter{User: vc.User, IncludeObserver: true}
|
|
|
|
|
2022-11-15 14:08:05 +00:00
|
|
|
if !license.IsPremium(ctx) {
|
2022-09-21 19:16:31 +00:00
|
|
|
// the low disk space filter is premium-only
|
|
|
|
opt.LowDiskSpaceFilter = nil
|
|
|
|
}
|
|
|
|
|
2021-10-11 14:37:48 +00:00
|
|
|
return svc.ds.ListHosts(ctx, filter, opt)
|
|
|
|
}
|
|
|
|
|
2021-09-29 16:13:23 +00:00
|
|
|
/////////////////////////////////////////////////////////////////////////////////
|
2021-12-14 21:34:11 +00:00
|
|
|
// Delete Hosts
|
2021-09-29 16:13:23 +00:00
|
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
type deleteHostsRequest struct {
|
|
|
|
IDs []uint `json:"ids"`
|
|
|
|
Filters struct {
|
|
|
|
MatchQuery string `json:"query"`
|
|
|
|
Status fleet.HostStatus `json:"status"`
|
|
|
|
LabelID *uint `json:"label_id"`
|
|
|
|
TeamID *uint `json:"team_id"`
|
|
|
|
} `json:"filters"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type deleteHostsResponse struct {
|
|
|
|
Err error `json:"error,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r deleteHostsResponse) error() error { return r.Err }
|
|
|
|
|
2022-12-27 14:26:59 +00:00
|
|
|
func deleteHostsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
2021-09-29 16:13:23 +00:00
|
|
|
req := request.(*deleteHostsRequest)
|
|
|
|
listOpt := fleet.HostListOptions{
|
|
|
|
ListOptions: fleet.ListOptions{
|
|
|
|
MatchQuery: req.Filters.MatchQuery,
|
|
|
|
},
|
|
|
|
StatusFilter: req.Filters.Status,
|
|
|
|
TeamFilter: req.Filters.TeamID,
|
|
|
|
}
|
|
|
|
err := svc.DeleteHosts(ctx, req.IDs, listOpt, req.Filters.LabelID)
|
|
|
|
if err != nil {
|
|
|
|
return deleteHostsResponse{Err: err}, nil
|
|
|
|
}
|
|
|
|
return deleteHostsResponse{}, nil
|
|
|
|
}
|
|
|
|
|
2021-12-14 21:34:11 +00:00
|
|
|
func (svc *Service) DeleteHosts(ctx context.Context, ids []uint, opts fleet.HostListOptions, lid *uint) error {
|
2021-10-06 15:58:01 +00:00
|
|
|
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
|
2021-09-29 16:13:23 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(ids) > 0 && (lid != nil || !opts.Empty()) {
|
2022-09-19 17:53:44 +00:00
|
|
|
return &fleet.BadRequestError{Message: "Cannot specify a list of ids and filters at the same time"}
|
2021-09-29 16:13:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if len(ids) > 0 {
|
2021-10-06 15:58:01 +00:00
|
|
|
err := svc.checkWriteForHostIDs(ctx, ids)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2021-09-29 16:13:23 +00:00
|
|
|
return svc.ds.DeleteHosts(ctx, ids)
|
|
|
|
}
|
|
|
|
|
|
|
|
hostIDs, err := svc.hostIDsFromFilters(ctx, opts, lid)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(hostIDs) == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
2021-10-06 15:58:01 +00:00
|
|
|
|
|
|
|
err = svc.checkWriteForHostIDs(ctx, hostIDs)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2021-09-29 16:13:23 +00:00
|
|
|
return svc.ds.DeleteHosts(ctx, hostIDs)
|
|
|
|
}
|
2021-10-06 15:58:01 +00:00
|
|
|
|
2021-10-07 11:25:35 +00:00
|
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Count
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
type countHostsRequest struct {
|
|
|
|
Opts fleet.HostListOptions `url:"host_options"`
|
|
|
|
LabelID *uint `query:"label_id,optional"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type countHostsResponse struct {
|
|
|
|
Count int `json:"count"`
|
|
|
|
Err error `json:"error,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r countHostsResponse) error() error { return r.Err }
|
|
|
|
|
2022-12-27 14:26:59 +00:00
|
|
|
func countHostsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
2021-10-07 11:25:35 +00:00
|
|
|
req := request.(*countHostsRequest)
|
|
|
|
count, err := svc.CountHosts(ctx, req.LabelID, req.Opts)
|
|
|
|
if err != nil {
|
|
|
|
return countHostsResponse{Err: err}, nil
|
|
|
|
}
|
|
|
|
return countHostsResponse{Count: count}, nil
|
|
|
|
}
|
|
|
|
|
2021-12-14 21:34:11 +00:00
|
|
|
func (svc *Service) CountHosts(ctx context.Context, labelID *uint, opts fleet.HostListOptions) (int, error) {
|
2021-10-07 17:25:04 +00:00
|
|
|
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
|
2021-10-07 11:25:35 +00:00
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return svc.countHostFromFilters(ctx, labelID, opts)
|
|
|
|
}
|
|
|
|
|
2021-12-14 21:34:11 +00:00
|
|
|
func (svc *Service) countHostFromFilters(ctx context.Context, labelID *uint, opt fleet.HostListOptions) (int, error) {
|
|
|
|
filter, err := processHostFilters(ctx, opt, nil)
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
|
2022-11-15 14:08:05 +00:00
|
|
|
if !license.IsPremium(ctx) {
|
2022-09-21 19:16:31 +00:00
|
|
|
// the low disk space filter is premium-only
|
|
|
|
opt.LowDiskSpaceFilter = nil
|
|
|
|
}
|
|
|
|
|
2021-12-14 21:34:11 +00:00
|
|
|
var count int
|
|
|
|
if labelID != nil {
|
|
|
|
count, err = svc.ds.CountHostsInLabel(ctx, filter, *labelID, opt)
|
|
|
|
} else {
|
|
|
|
count, err = svc.ds.CountHosts(ctx, filter, opt)
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return count, nil
|
|
|
|
}
|
|
|
|
|
2022-06-10 18:29:45 +00:00
|
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Search
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
type searchHostsRequest struct {
|
|
|
|
// MatchQuery is the query SQL
|
|
|
|
MatchQuery string `json:"query"`
|
|
|
|
// QueryID is the ID of a saved query to run (used to determine if this is a
|
|
|
|
// query that observers can run).
|
|
|
|
QueryID *uint `json:"query_id"`
|
|
|
|
// ExcludedHostIDs is the list of IDs selected on the caller side
|
|
|
|
// (e.g. the UI) that will be excluded from the returned payload.
|
|
|
|
ExcludedHostIDs []uint `json:"excluded_host_ids"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type searchHostsResponse struct {
|
2022-10-14 14:14:18 +00:00
|
|
|
Hosts []*fleet.HostResponse `json:"hosts"`
|
|
|
|
Err error `json:"error,omitempty"`
|
2022-06-10 18:29:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (r searchHostsResponse) error() error { return r.Err }
|
|
|
|
|
2022-12-27 14:26:59 +00:00
|
|
|
func searchHostsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
2022-06-10 18:29:45 +00:00
|
|
|
req := request.(*searchHostsRequest)
|
|
|
|
|
|
|
|
hosts, err := svc.SearchHosts(ctx, req.MatchQuery, req.QueryID, req.ExcludedHostIDs)
|
|
|
|
if err != nil {
|
|
|
|
return searchHostsResponse{Err: err}, nil
|
|
|
|
}
|
|
|
|
|
2022-10-14 14:14:18 +00:00
|
|
|
results := []*fleet.HostResponse{}
|
2022-06-10 18:29:45 +00:00
|
|
|
|
|
|
|
for _, h := range hosts {
|
2022-10-14 14:14:18 +00:00
|
|
|
results = append(results, fleet.HostResponseForHostCheap(h))
|
2022-06-10 18:29:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return searchHostsResponse{
|
|
|
|
Hosts: results,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (svc *Service) SearchHosts(ctx context.Context, matchQuery string, queryID *uint, excludedHostIDs []uint) ([]*fleet.Host, error) {
|
|
|
|
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionRead); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
vc, ok := viewer.FromContext(ctx)
|
|
|
|
if !ok {
|
|
|
|
return nil, fleet.ErrNoContext
|
|
|
|
}
|
|
|
|
|
|
|
|
includeObserver := false
|
|
|
|
if queryID != nil {
|
|
|
|
canRun, err := svc.ds.ObserverCanRunQuery(ctx, *queryID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
includeObserver = canRun
|
|
|
|
}
|
|
|
|
|
|
|
|
filter := fleet.TeamFilter{User: vc.User, IncludeObserver: includeObserver}
|
|
|
|
|
|
|
|
results := []*fleet.Host{}
|
|
|
|
|
|
|
|
hosts, err := svc.ds.SearchHosts(ctx, filter, matchQuery, excludedHostIDs...)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
results = append(results, hosts...)
|
|
|
|
|
|
|
|
return results, nil
|
|
|
|
}
|
|
|
|
|
2021-10-07 11:25:35 +00:00
|
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Get host
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
2021-12-14 21:34:11 +00:00
|
|
|
type getHostRequest struct {
|
|
|
|
ID uint `url:"id"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type getHostResponse struct {
|
|
|
|
Host *HostDetailResponse `json:"host"`
|
|
|
|
Err error `json:"error,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r getHostResponse) error() error { return r.Err }
|
|
|
|
|
2022-12-27 14:26:59 +00:00
|
|
|
func getHostEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
2021-10-07 11:25:35 +00:00
|
|
|
req := request.(*getHostRequest)
|
only include policies in device endpoints for premium users (#6077)
This removes policy information from `GET /api/_version_/fleet/device/{token}` from non-premium Fleet instances.
Starting the server with `./build/fleet serve --dev --dev_license`
```bash
$ curl -s https://localhost:8080/api/latest/fleet/device/1804e808-171f-4dda-9bec-f695b2f2371a | jq '.host.policies // "not present"'
[
{
"id": 3,
"name": "Antivirus healthy (Linux)",
"query": "SELECT score FROM (SELECT case when COUNT(*) = 2 then 1 ELSE 0 END AS score FROM processes WHERE (name = 'clamd') OR (name = 'freshclam')) WHERE score == 1;",
"description": "Checks that both ClamAV's daemon and its updater service (freshclam) are running.",
"author_id": 1,
"author_name": "Roberto",
"author_email": "test@example.com",
"team_id": null,
"resolution": "Ensure ClamAV and Freshclam are installed and running.",
"platform": "darwin,linux",
"created_at": "2022-05-23T20:53:36Z",
"updated_at": "2022-06-03T13:17:42Z",
"response": ""
}
]
```
Starting the server with `./build/fleet serve --dev`
```bash
$ curl -s https://localhost:8080/api/latest/fleet/device/1804e808-171f-4dda-9bec-f695b2f2371a | jq '.host.policies // "not present"'
"not present"
```
2022-06-07 16:27:13 +00:00
|
|
|
opts := fleet.HostDetailOptions{
|
|
|
|
IncludeCVEScores: false,
|
|
|
|
IncludePolicies: true, // intentionally true to preserve existing behavior
|
|
|
|
}
|
|
|
|
host, err := svc.GetHost(ctx, req.ID, opts)
|
2021-10-07 11:25:35 +00:00
|
|
|
if err != nil {
|
|
|
|
return getHostResponse{Err: err}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err := hostDetailResponseForHost(ctx, svc, host)
|
|
|
|
if err != nil {
|
|
|
|
return getHostResponse{Err: err}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return getHostResponse{Host: resp}, nil
|
|
|
|
}
|
|
|
|
|
only include policies in device endpoints for premium users (#6077)
This removes policy information from `GET /api/_version_/fleet/device/{token}` from non-premium Fleet instances.
Starting the server with `./build/fleet serve --dev --dev_license`
```bash
$ curl -s https://localhost:8080/api/latest/fleet/device/1804e808-171f-4dda-9bec-f695b2f2371a | jq '.host.policies // "not present"'
[
{
"id": 3,
"name": "Antivirus healthy (Linux)",
"query": "SELECT score FROM (SELECT case when COUNT(*) = 2 then 1 ELSE 0 END AS score FROM processes WHERE (name = 'clamd') OR (name = 'freshclam')) WHERE score == 1;",
"description": "Checks that both ClamAV's daemon and its updater service (freshclam) are running.",
"author_id": 1,
"author_name": "Roberto",
"author_email": "test@example.com",
"team_id": null,
"resolution": "Ensure ClamAV and Freshclam are installed and running.",
"platform": "darwin,linux",
"created_at": "2022-05-23T20:53:36Z",
"updated_at": "2022-06-03T13:17:42Z",
"response": ""
}
]
```
Starting the server with `./build/fleet serve --dev`
```bash
$ curl -s https://localhost:8080/api/latest/fleet/device/1804e808-171f-4dda-9bec-f695b2f2371a | jq '.host.policies // "not present"'
"not present"
```
2022-06-07 16:27:13 +00:00
|
|
|
func (svc *Service) GetHost(ctx context.Context, id uint, opts fleet.HostDetailOptions) (*fleet.HostDetail, error) {
|
2023-02-08 23:20:23 +00:00
|
|
|
alreadyAuthd := svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceToken)
|
2022-03-09 21:13:56 +00:00
|
|
|
if !alreadyAuthd {
|
|
|
|
// First ensure the user has access to list hosts, then check the specific
|
|
|
|
// host once team_id is loaded.
|
|
|
|
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2021-12-14 21:34:11 +00:00
|
|
|
}
|
|
|
|
|
2022-05-25 16:30:03 +00:00
|
|
|
host, err := svc.ds.Host(ctx, id)
|
2021-12-14 21:34:11 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, ctxerr.Wrap(ctx, err, "get host")
|
|
|
|
}
|
|
|
|
|
2022-03-09 21:13:56 +00:00
|
|
|
if !alreadyAuthd {
|
|
|
|
// Authorize again with team loaded now that we have team_id
|
|
|
|
if err := svc.authz.Authorize(ctx, host, fleet.ActionRead); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2021-12-14 21:34:11 +00:00
|
|
|
}
|
|
|
|
|
only include policies in device endpoints for premium users (#6077)
This removes policy information from `GET /api/_version_/fleet/device/{token}` from non-premium Fleet instances.
Starting the server with `./build/fleet serve --dev --dev_license`
```bash
$ curl -s https://localhost:8080/api/latest/fleet/device/1804e808-171f-4dda-9bec-f695b2f2371a | jq '.host.policies // "not present"'
[
{
"id": 3,
"name": "Antivirus healthy (Linux)",
"query": "SELECT score FROM (SELECT case when COUNT(*) = 2 then 1 ELSE 0 END AS score FROM processes WHERE (name = 'clamd') OR (name = 'freshclam')) WHERE score == 1;",
"description": "Checks that both ClamAV's daemon and its updater service (freshclam) are running.",
"author_id": 1,
"author_name": "Roberto",
"author_email": "test@example.com",
"team_id": null,
"resolution": "Ensure ClamAV and Freshclam are installed and running.",
"platform": "darwin,linux",
"created_at": "2022-05-23T20:53:36Z",
"updated_at": "2022-06-03T13:17:42Z",
"response": ""
}
]
```
Starting the server with `./build/fleet serve --dev`
```bash
$ curl -s https://localhost:8080/api/latest/fleet/device/1804e808-171f-4dda-9bec-f695b2f2371a | jq '.host.policies // "not present"'
"not present"
```
2022-06-07 16:27:13 +00:00
|
|
|
hostDetails, err := svc.getHostDetails(ctx, host, opts)
|
2022-05-20 16:58:40 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return hostDetails, nil
|
2021-12-14 21:34:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (svc *Service) checkWriteForHostIDs(ctx context.Context, ids []uint) error {
|
2021-10-06 15:58:01 +00:00
|
|
|
for _, id := range ids {
|
2022-01-18 01:52:09 +00:00
|
|
|
host, err := svc.ds.HostLite(ctx, id)
|
2021-10-06 15:58:01 +00:00
|
|
|
if err != nil {
|
2021-11-22 14:13:26 +00:00
|
|
|
return ctxerr.Wrap(ctx, err, "get host for delete")
|
2021-10-06 15:58:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Authorize again with team loaded now that we have team_id
|
|
|
|
if err := svc.authz.Authorize(ctx, host, fleet.ActionWrite); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
2021-11-09 14:35:36 +00:00
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Get Host Summary
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
type getHostSummaryRequest struct {
|
2022-09-21 19:56:17 +00:00
|
|
|
TeamID *uint `query:"team_id,optional"`
|
|
|
|
Platform *string `query:"platform,optional"`
|
|
|
|
LowDiskSpace *int `query:"low_disk_space,optional"`
|
2021-11-09 14:35:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type getHostSummaryResponse struct {
|
|
|
|
fleet.HostSummary
|
|
|
|
Err error `json:"error,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r getHostSummaryResponse) error() error { return r.Err }
|
|
|
|
|
2022-12-27 14:26:59 +00:00
|
|
|
func getHostSummaryEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
2021-11-09 14:35:36 +00:00
|
|
|
req := request.(*getHostSummaryRequest)
|
2022-09-21 19:56:17 +00:00
|
|
|
if req.LowDiskSpace != nil {
|
|
|
|
if *req.LowDiskSpace < 1 || *req.LowDiskSpace > 100 {
|
|
|
|
err := ctxerr.Errorf(ctx, "invalid low_disk_space threshold, must be between 1 and 100: %d", *req.LowDiskSpace)
|
|
|
|
return getHostSummaryResponse{Err: err}, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
summary, err := svc.GetHostSummary(ctx, req.TeamID, req.Platform, req.LowDiskSpace)
|
2021-11-09 14:35:36 +00:00
|
|
|
if err != nil {
|
|
|
|
return getHostSummaryResponse{Err: err}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
resp := getHostSummaryResponse{
|
|
|
|
HostSummary: *summary,
|
|
|
|
}
|
|
|
|
return resp, nil
|
|
|
|
}
|
|
|
|
|
2022-09-21 19:56:17 +00:00
|
|
|
func (svc *Service) GetHostSummary(ctx context.Context, teamID *uint, platform *string, lowDiskSpace *int) (*fleet.HostSummary, error) {
|
2021-11-09 14:35:36 +00:00
|
|
|
if err := svc.authz.Authorize(ctx, &fleet.Host{TeamID: teamID}, fleet.ActionList); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
vc, ok := viewer.FromContext(ctx)
|
|
|
|
if !ok {
|
|
|
|
return nil, fleet.ErrNoContext
|
|
|
|
}
|
|
|
|
filter := fleet.TeamFilter{User: vc.User, IncludeObserver: true, TeamID: teamID}
|
|
|
|
|
2022-11-15 14:08:05 +00:00
|
|
|
if !license.IsPremium(ctx) {
|
2022-09-21 19:56:17 +00:00
|
|
|
lowDiskSpace = nil
|
|
|
|
}
|
|
|
|
|
|
|
|
hostSummary, err := svc.ds.GenerateHostStatusStatistics(ctx, filter, svc.clock.Now(), platform, lowDiskSpace)
|
2021-11-09 14:35:36 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2022-05-10 15:32:55 +00:00
|
|
|
|
|
|
|
linuxCount := uint(0)
|
|
|
|
for _, p := range hostSummary.Platforms {
|
|
|
|
if fleet.IsLinux(p.Platform) {
|
|
|
|
linuxCount += p.HostsCount
|
|
|
|
}
|
|
|
|
}
|
|
|
|
hostSummary.AllLinuxCount = linuxCount
|
|
|
|
|
|
|
|
labelsSummary, err := svc.ds.LabelsSummary(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: should query for "All linux" label be updated to use `platform` from `os_version` table
|
|
|
|
// so that the label tracks the way platforms are handled here in the host summary?
|
|
|
|
var builtinLabels []*fleet.LabelSummary
|
|
|
|
for _, l := range labelsSummary {
|
|
|
|
if l.LabelType == fleet.LabelTypeBuiltIn {
|
|
|
|
builtinLabels = append(builtinLabels, l)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
hostSummary.BuiltinLabels = builtinLabels
|
|
|
|
|
|
|
|
return hostSummary, nil
|
2021-11-09 14:35:36 +00:00
|
|
|
}
|
2021-12-14 21:34:11 +00:00
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Get Host By Identifier
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
type hostByIdentifierRequest struct {
|
|
|
|
Identifier string `url:"identifier"`
|
|
|
|
}
|
|
|
|
|
2022-12-27 14:26:59 +00:00
|
|
|
func hostByIdentifierEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
2021-12-14 21:34:11 +00:00
|
|
|
req := request.(*hostByIdentifierRequest)
|
only include policies in device endpoints for premium users (#6077)
This removes policy information from `GET /api/_version_/fleet/device/{token}` from non-premium Fleet instances.
Starting the server with `./build/fleet serve --dev --dev_license`
```bash
$ curl -s https://localhost:8080/api/latest/fleet/device/1804e808-171f-4dda-9bec-f695b2f2371a | jq '.host.policies // "not present"'
[
{
"id": 3,
"name": "Antivirus healthy (Linux)",
"query": "SELECT score FROM (SELECT case when COUNT(*) = 2 then 1 ELSE 0 END AS score FROM processes WHERE (name = 'clamd') OR (name = 'freshclam')) WHERE score == 1;",
"description": "Checks that both ClamAV's daemon and its updater service (freshclam) are running.",
"author_id": 1,
"author_name": "Roberto",
"author_email": "test@example.com",
"team_id": null,
"resolution": "Ensure ClamAV and Freshclam are installed and running.",
"platform": "darwin,linux",
"created_at": "2022-05-23T20:53:36Z",
"updated_at": "2022-06-03T13:17:42Z",
"response": ""
}
]
```
Starting the server with `./build/fleet serve --dev`
```bash
$ curl -s https://localhost:8080/api/latest/fleet/device/1804e808-171f-4dda-9bec-f695b2f2371a | jq '.host.policies // "not present"'
"not present"
```
2022-06-07 16:27:13 +00:00
|
|
|
opts := fleet.HostDetailOptions{
|
|
|
|
IncludeCVEScores: false,
|
|
|
|
IncludePolicies: true, // intentionally true to preserve existing behavior
|
|
|
|
}
|
|
|
|
host, err := svc.HostByIdentifier(ctx, req.Identifier, opts)
|
2021-12-14 21:34:11 +00:00
|
|
|
if err != nil {
|
|
|
|
return getHostResponse{Err: err}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err := hostDetailResponseForHost(ctx, svc, host)
|
|
|
|
if err != nil {
|
|
|
|
return getHostResponse{Err: err}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return getHostResponse{
|
|
|
|
Host: resp,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
only include policies in device endpoints for premium users (#6077)
This removes policy information from `GET /api/_version_/fleet/device/{token}` from non-premium Fleet instances.
Starting the server with `./build/fleet serve --dev --dev_license`
```bash
$ curl -s https://localhost:8080/api/latest/fleet/device/1804e808-171f-4dda-9bec-f695b2f2371a | jq '.host.policies // "not present"'
[
{
"id": 3,
"name": "Antivirus healthy (Linux)",
"query": "SELECT score FROM (SELECT case when COUNT(*) = 2 then 1 ELSE 0 END AS score FROM processes WHERE (name = 'clamd') OR (name = 'freshclam')) WHERE score == 1;",
"description": "Checks that both ClamAV's daemon and its updater service (freshclam) are running.",
"author_id": 1,
"author_name": "Roberto",
"author_email": "test@example.com",
"team_id": null,
"resolution": "Ensure ClamAV and Freshclam are installed and running.",
"platform": "darwin,linux",
"created_at": "2022-05-23T20:53:36Z",
"updated_at": "2022-06-03T13:17:42Z",
"response": ""
}
]
```
Starting the server with `./build/fleet serve --dev`
```bash
$ curl -s https://localhost:8080/api/latest/fleet/device/1804e808-171f-4dda-9bec-f695b2f2371a | jq '.host.policies // "not present"'
"not present"
```
2022-06-07 16:27:13 +00:00
|
|
|
func (svc *Service) HostByIdentifier(ctx context.Context, identifier string, opts fleet.HostDetailOptions) (*fleet.HostDetail, error) {
|
2021-12-14 21:34:11 +00:00
|
|
|
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
host, err := svc.ds.HostByIdentifier(ctx, identifier)
|
|
|
|
if err != nil {
|
|
|
|
return nil, ctxerr.Wrap(ctx, err, "get host by identifier")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Authorize again with team loaded now that we have team_id
|
|
|
|
if err := svc.authz.Authorize(ctx, host, fleet.ActionRead); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
only include policies in device endpoints for premium users (#6077)
This removes policy information from `GET /api/_version_/fleet/device/{token}` from non-premium Fleet instances.
Starting the server with `./build/fleet serve --dev --dev_license`
```bash
$ curl -s https://localhost:8080/api/latest/fleet/device/1804e808-171f-4dda-9bec-f695b2f2371a | jq '.host.policies // "not present"'
[
{
"id": 3,
"name": "Antivirus healthy (Linux)",
"query": "SELECT score FROM (SELECT case when COUNT(*) = 2 then 1 ELSE 0 END AS score FROM processes WHERE (name = 'clamd') OR (name = 'freshclam')) WHERE score == 1;",
"description": "Checks that both ClamAV's daemon and its updater service (freshclam) are running.",
"author_id": 1,
"author_name": "Roberto",
"author_email": "test@example.com",
"team_id": null,
"resolution": "Ensure ClamAV and Freshclam are installed and running.",
"platform": "darwin,linux",
"created_at": "2022-05-23T20:53:36Z",
"updated_at": "2022-06-03T13:17:42Z",
"response": ""
}
]
```
Starting the server with `./build/fleet serve --dev`
```bash
$ curl -s https://localhost:8080/api/latest/fleet/device/1804e808-171f-4dda-9bec-f695b2f2371a | jq '.host.policies // "not present"'
"not present"
```
2022-06-07 16:27:13 +00:00
|
|
|
hostDetails, err := svc.getHostDetails(ctx, host, opts)
|
2022-05-20 16:58:40 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return hostDetails, nil
|
2021-12-14 21:34:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Delete Host
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
type deleteHostRequest struct {
|
|
|
|
ID uint `url:"id"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type deleteHostResponse struct {
|
|
|
|
Err error `json:"error,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r deleteHostResponse) error() error { return r.Err }
|
|
|
|
|
2022-12-27 14:26:59 +00:00
|
|
|
func deleteHostEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
2021-12-14 21:34:11 +00:00
|
|
|
req := request.(*deleteHostRequest)
|
|
|
|
err := svc.DeleteHost(ctx, req.ID)
|
|
|
|
if err != nil {
|
|
|
|
return deleteHostResponse{Err: err}, nil
|
|
|
|
}
|
|
|
|
return deleteHostResponse{}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (svc *Service) DeleteHost(ctx context.Context, id uint) error {
|
|
|
|
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2022-01-18 01:52:09 +00:00
|
|
|
host, err := svc.ds.HostLite(ctx, id)
|
2021-12-14 21:34:11 +00:00
|
|
|
if err != nil {
|
|
|
|
return ctxerr.Wrap(ctx, err, "get host for delete")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Authorize again with team loaded now that we have team_id
|
|
|
|
if err := svc.authz.Authorize(ctx, host, fleet.ActionWrite); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return svc.ds.DeleteHost(ctx, id)
|
|
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Add Hosts to Team
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
type addHostsToTeamRequest struct {
|
|
|
|
TeamID *uint `json:"team_id"`
|
|
|
|
HostIDs []uint `json:"hosts"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type addHostsToTeamResponse struct {
|
|
|
|
Err error `json:"error,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r addHostsToTeamResponse) error() error { return r.Err }
|
|
|
|
|
2022-12-27 14:26:59 +00:00
|
|
|
func addHostsToTeamEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
2021-12-14 21:34:11 +00:00
|
|
|
req := request.(*addHostsToTeamRequest)
|
|
|
|
err := svc.AddHostsToTeam(ctx, req.TeamID, req.HostIDs)
|
|
|
|
if err != nil {
|
|
|
|
return addHostsToTeamResponse{Err: err}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return addHostsToTeamResponse{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (svc *Service) AddHostsToTeam(ctx context.Context, teamID *uint, hostIDs []uint) error {
|
|
|
|
// This is currently treated as a "team write". If we ever give users
|
|
|
|
// besides global admins permissions to modify team hosts, we will need to
|
|
|
|
// check that the user has permissions for both the source and destination
|
|
|
|
// teams.
|
|
|
|
if err := svc.authz.Authorize(ctx, &fleet.Host{TeamID: teamID}, fleet.ActionWrite); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return svc.ds.AddHostsToTeam(ctx, teamID, hostIDs)
|
|
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Add Hosts to Team by Filter
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
type addHostsToTeamByFilterRequest struct {
|
|
|
|
TeamID *uint `json:"team_id"`
|
|
|
|
Filters struct {
|
|
|
|
MatchQuery string `json:"query"`
|
|
|
|
Status fleet.HostStatus `json:"status"`
|
|
|
|
LabelID *uint `json:"label_id"`
|
|
|
|
} `json:"filters"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type addHostsToTeamByFilterResponse struct {
|
|
|
|
Err error `json:"error,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r addHostsToTeamByFilterResponse) error() error { return r.Err }
|
|
|
|
|
2022-12-27 14:26:59 +00:00
|
|
|
func addHostsToTeamByFilterEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
2021-12-14 21:34:11 +00:00
|
|
|
req := request.(*addHostsToTeamByFilterRequest)
|
|
|
|
listOpt := fleet.HostListOptions{
|
|
|
|
ListOptions: fleet.ListOptions{
|
|
|
|
MatchQuery: req.Filters.MatchQuery,
|
|
|
|
},
|
|
|
|
StatusFilter: req.Filters.Status,
|
|
|
|
}
|
|
|
|
err := svc.AddHostsToTeamByFilter(ctx, req.TeamID, listOpt, req.Filters.LabelID)
|
|
|
|
if err != nil {
|
|
|
|
return addHostsToTeamByFilterResponse{Err: err}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return addHostsToTeamByFilterResponse{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (svc *Service) AddHostsToTeamByFilter(ctx context.Context, teamID *uint, opt fleet.HostListOptions, lid *uint) error {
|
|
|
|
// This is currently treated as a "team write". If we ever give users
|
|
|
|
// besides global admins permissions to modify team hosts, we will need to
|
|
|
|
// check that the user has permissions for both the source and destination
|
|
|
|
// teams.
|
|
|
|
if err := svc.authz.Authorize(ctx, &fleet.Host{TeamID: teamID}, fleet.ActionWrite); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
hostIDs, err := svc.hostIDsFromFilters(ctx, opt, lid)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if len(hostIDs) == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Apply the team to the selected hosts.
|
|
|
|
return svc.ds.AddHostsToTeam(ctx, teamID, hostIDs)
|
|
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Refetch Host
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
type refetchHostRequest struct {
|
|
|
|
ID uint `url:"id"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type refetchHostResponse struct {
|
|
|
|
Err error `json:"error,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r refetchHostResponse) error() error { return r.Err }
|
|
|
|
|
2022-12-27 14:26:59 +00:00
|
|
|
func refetchHostEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
2021-12-14 21:34:11 +00:00
|
|
|
req := request.(*refetchHostRequest)
|
|
|
|
err := svc.RefetchHost(ctx, req.ID)
|
|
|
|
if err != nil {
|
|
|
|
return refetchHostResponse{Err: err}, nil
|
|
|
|
}
|
|
|
|
return refetchHostResponse{}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (svc *Service) RefetchHost(ctx context.Context, id uint) error {
|
2023-02-08 23:20:23 +00:00
|
|
|
if !svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceToken) {
|
2022-03-09 21:13:56 +00:00
|
|
|
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2021-12-14 21:34:11 +00:00
|
|
|
|
2022-03-09 21:13:56 +00:00
|
|
|
host, err := svc.ds.HostLite(ctx, id)
|
|
|
|
if err != nil {
|
|
|
|
return ctxerr.Wrap(ctx, err, "find host for refetch")
|
|
|
|
}
|
2021-12-14 21:34:11 +00:00
|
|
|
|
2022-03-09 21:13:56 +00:00
|
|
|
// We verify fleet.ActionRead instead of fleet.ActionWrite because we want to allow
|
|
|
|
// observers to be able to refetch hosts.
|
|
|
|
if err := svc.authz.Authorize(ctx, host, fleet.ActionRead); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2021-12-14 21:34:11 +00:00
|
|
|
}
|
|
|
|
|
2022-03-09 21:13:56 +00:00
|
|
|
if err := svc.ds.UpdateHostRefetchRequested(ctx, id, true); err != nil {
|
2021-12-14 21:34:11 +00:00
|
|
|
return ctxerr.Wrap(ctx, err, "save host")
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
only include policies in device endpoints for premium users (#6077)
This removes policy information from `GET /api/_version_/fleet/device/{token}` from non-premium Fleet instances.
Starting the server with `./build/fleet serve --dev --dev_license`
```bash
$ curl -s https://localhost:8080/api/latest/fleet/device/1804e808-171f-4dda-9bec-f695b2f2371a | jq '.host.policies // "not present"'
[
{
"id": 3,
"name": "Antivirus healthy (Linux)",
"query": "SELECT score FROM (SELECT case when COUNT(*) = 2 then 1 ELSE 0 END AS score FROM processes WHERE (name = 'clamd') OR (name = 'freshclam')) WHERE score == 1;",
"description": "Checks that both ClamAV's daemon and its updater service (freshclam) are running.",
"author_id": 1,
"author_name": "Roberto",
"author_email": "test@example.com",
"team_id": null,
"resolution": "Ensure ClamAV and Freshclam are installed and running.",
"platform": "darwin,linux",
"created_at": "2022-05-23T20:53:36Z",
"updated_at": "2022-06-03T13:17:42Z",
"response": ""
}
]
```
Starting the server with `./build/fleet serve --dev`
```bash
$ curl -s https://localhost:8080/api/latest/fleet/device/1804e808-171f-4dda-9bec-f695b2f2371a | jq '.host.policies // "not present"'
"not present"
```
2022-06-07 16:27:13 +00:00
|
|
|
func (svc *Service) getHostDetails(ctx context.Context, host *fleet.Host, opts fleet.HostDetailOptions) (*fleet.HostDetail, error) {
|
|
|
|
if err := svc.ds.LoadHostSoftware(ctx, host, opts.IncludeCVEScores); err != nil {
|
2021-12-14 21:34:11 +00:00
|
|
|
return nil, ctxerr.Wrap(ctx, err, "load host software")
|
|
|
|
}
|
|
|
|
|
|
|
|
labels, err := svc.ds.ListLabelsForHost(ctx, host.ID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, ctxerr.Wrap(ctx, err, "get labels for host")
|
|
|
|
}
|
|
|
|
|
|
|
|
packs, err := svc.ds.ListPacksForHost(ctx, host.ID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, ctxerr.Wrap(ctx, err, "get packs for host")
|
|
|
|
}
|
|
|
|
|
2022-06-28 18:11:49 +00:00
|
|
|
bats, err := svc.ds.ListHostBatteries(ctx, host.ID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, ctxerr.Wrap(ctx, err, "get batteries for host")
|
|
|
|
}
|
|
|
|
|
2022-07-21 02:16:03 +00:00
|
|
|
// Due to a known osquery issue with M1 Macs, we are ignoring the stored value in the db
|
|
|
|
// and replacing it at the service layer with custom values determined by the cycle count.
|
|
|
|
// See https://github.com/fleetdm/fleet/issues/6763.
|
|
|
|
// TODO: Update once the underlying osquery issue has been resolved.
|
|
|
|
for _, b := range bats {
|
|
|
|
if b.CycleCount < 1000 {
|
|
|
|
b.Health = "Normal"
|
|
|
|
} else {
|
|
|
|
b.Health = "Replacement recommended"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
only include policies in device endpoints for premium users (#6077)
This removes policy information from `GET /api/_version_/fleet/device/{token}` from non-premium Fleet instances.
Starting the server with `./build/fleet serve --dev --dev_license`
```bash
$ curl -s https://localhost:8080/api/latest/fleet/device/1804e808-171f-4dda-9bec-f695b2f2371a | jq '.host.policies // "not present"'
[
{
"id": 3,
"name": "Antivirus healthy (Linux)",
"query": "SELECT score FROM (SELECT case when COUNT(*) = 2 then 1 ELSE 0 END AS score FROM processes WHERE (name = 'clamd') OR (name = 'freshclam')) WHERE score == 1;",
"description": "Checks that both ClamAV's daemon and its updater service (freshclam) are running.",
"author_id": 1,
"author_name": "Roberto",
"author_email": "test@example.com",
"team_id": null,
"resolution": "Ensure ClamAV and Freshclam are installed and running.",
"platform": "darwin,linux",
"created_at": "2022-05-23T20:53:36Z",
"updated_at": "2022-06-03T13:17:42Z",
"response": ""
}
]
```
Starting the server with `./build/fleet serve --dev`
```bash
$ curl -s https://localhost:8080/api/latest/fleet/device/1804e808-171f-4dda-9bec-f695b2f2371a | jq '.host.policies // "not present"'
"not present"
```
2022-06-07 16:27:13 +00:00
|
|
|
var policies *[]*fleet.HostPolicy
|
|
|
|
if opts.IncludePolicies {
|
|
|
|
hp, err := svc.ds.ListPoliciesForHost(ctx, host)
|
|
|
|
if err != nil {
|
|
|
|
return nil, ctxerr.Wrap(ctx, err, "get policies for host")
|
|
|
|
}
|
|
|
|
|
|
|
|
if hp == nil {
|
|
|
|
hp = []*fleet.HostPolicy{}
|
|
|
|
}
|
|
|
|
|
|
|
|
policies = &hp
|
2021-12-14 21:34:11 +00:00
|
|
|
}
|
|
|
|
|
2023-02-22 22:26:06 +00:00
|
|
|
// If Fleet MDM is enabled and configured, we want to include MDM profiles.
|
|
|
|
ac, err := svc.ds.AppConfig(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return nil, ctxerr.Wrap(ctx, err, "get app config for host mdm profiles")
|
|
|
|
}
|
|
|
|
if ac.MDM.EnabledAndConfigured {
|
|
|
|
profiles, err := svc.ds.GetHostMDMProfiles(ctx, host.UUID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, ctxerr.Wrap(ctx, err, "get host mdm profiles")
|
|
|
|
}
|
|
|
|
host.MDM.Profiles = &profiles
|
|
|
|
}
|
|
|
|
|
2022-06-28 18:11:49 +00:00
|
|
|
return &fleet.HostDetail{
|
|
|
|
Host: *host,
|
|
|
|
Labels: labels,
|
|
|
|
Packs: packs,
|
|
|
|
Policies: policies,
|
|
|
|
Batteries: &bats,
|
|
|
|
}, nil
|
2021-12-14 21:34:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (svc *Service) hostIDsFromFilters(ctx context.Context, opt fleet.HostListOptions, lid *uint) ([]uint, error) {
|
|
|
|
filter, err := processHostFilters(ctx, opt, lid)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Load hosts, either from label if provided or from all hosts.
|
|
|
|
var hosts []*fleet.Host
|
|
|
|
if lid != nil {
|
|
|
|
hosts, err = svc.ds.ListHostsInLabel(ctx, filter, *lid, opt)
|
|
|
|
} else {
|
|
|
|
hosts, err = svc.ds.ListHosts(ctx, filter, opt)
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(hosts) == 0 {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
hostIDs := make([]uint, 0, len(hosts))
|
|
|
|
for _, h := range hosts {
|
|
|
|
hostIDs = append(hostIDs, h.ID)
|
|
|
|
}
|
|
|
|
return hostIDs, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func processHostFilters(ctx context.Context, opt fleet.HostListOptions, lid *uint) (fleet.TeamFilter, error) {
|
|
|
|
vc, ok := viewer.FromContext(ctx)
|
|
|
|
if !ok {
|
|
|
|
return fleet.TeamFilter{}, fleet.ErrNoContext
|
|
|
|
}
|
|
|
|
filter := fleet.TeamFilter{User: vc.User, IncludeObserver: true}
|
|
|
|
|
|
|
|
if opt.StatusFilter != "" && lid != nil {
|
|
|
|
return fleet.TeamFilter{}, fleet.NewInvalidArgumentError("status", "may not be provided with label_id")
|
|
|
|
}
|
|
|
|
|
|
|
|
opt.PerPage = fleet.PerPageUnlimited
|
|
|
|
return filter, nil
|
|
|
|
}
|
2021-12-21 12:37:58 +00:00
|
|
|
|
2021-12-21 20:36:19 +00:00
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// List Host Device Mappings
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
type listHostDeviceMappingRequest struct {
|
|
|
|
ID uint `url:"id"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type listHostDeviceMappingResponse struct {
|
|
|
|
HostID uint `json:"host_id"`
|
|
|
|
DeviceMapping []*fleet.HostDeviceMapping `json:"device_mapping"`
|
|
|
|
Err error `json:"error,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r listHostDeviceMappingResponse) error() error { return r.Err }
|
|
|
|
|
2022-12-27 14:26:59 +00:00
|
|
|
func listHostDeviceMappingEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
2021-12-21 20:36:19 +00:00
|
|
|
req := request.(*listHostDeviceMappingRequest)
|
|
|
|
dms, err := svc.ListHostDeviceMapping(ctx, req.ID)
|
|
|
|
if err != nil {
|
|
|
|
return listHostDeviceMappingResponse{Err: err}, nil
|
|
|
|
}
|
|
|
|
return listHostDeviceMappingResponse{HostID: req.ID, DeviceMapping: dms}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (svc *Service) ListHostDeviceMapping(ctx context.Context, id uint) ([]*fleet.HostDeviceMapping, error) {
|
2023-02-08 23:20:23 +00:00
|
|
|
if !svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceToken) {
|
2022-03-09 21:13:56 +00:00
|
|
|
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2021-12-21 20:36:19 +00:00
|
|
|
|
2022-03-09 21:13:56 +00:00
|
|
|
host, err := svc.ds.HostLite(ctx, id)
|
|
|
|
if err != nil {
|
|
|
|
return nil, ctxerr.Wrap(ctx, err, "get host")
|
|
|
|
}
|
2021-12-21 20:36:19 +00:00
|
|
|
|
2022-03-09 21:13:56 +00:00
|
|
|
// Authorize again with team loaded now that we have team_id
|
|
|
|
if err := svc.authz.Authorize(ctx, host, fleet.ActionRead); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2021-12-21 20:36:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return svc.ds.ListHostDeviceMapping(ctx, id)
|
|
|
|
}
|
|
|
|
|
2022-11-01 17:22:07 +00:00
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// MDM
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
type getHostMDMRequest struct {
|
|
|
|
ID uint `url:"id"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type getHostMDMResponse struct {
|
|
|
|
*fleet.HostMDM
|
2022-12-27 19:22:37 +00:00
|
|
|
Err error `json:"error,omitempty"`
|
2022-11-01 17:22:07 +00:00
|
|
|
}
|
|
|
|
|
2022-12-27 14:26:59 +00:00
|
|
|
func (r getHostMDMResponse) error() error { return r.Err }
|
|
|
|
|
|
|
|
func getHostMDM(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
2022-11-01 17:22:07 +00:00
|
|
|
req := request.(*getHostMDMRequest)
|
|
|
|
mdm, err := svc.MDMData(ctx, req.ID)
|
|
|
|
if err != nil {
|
|
|
|
return getHostMDMResponse{Err: err}, nil
|
|
|
|
}
|
|
|
|
return getHostMDMResponse{HostMDM: mdm}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type getHostMDMSummaryResponse struct {
|
|
|
|
fleet.AggregatedMDMData
|
|
|
|
Err error `json:"error,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type getHostMDMSummaryRequest struct {
|
|
|
|
TeamID *uint `query:"team_id,optional"`
|
|
|
|
Platform string `query:"platform,optional"`
|
|
|
|
}
|
|
|
|
|
2022-12-27 14:26:59 +00:00
|
|
|
func (r getHostMDMSummaryResponse) error() error { return r.Err }
|
|
|
|
|
|
|
|
func getHostMDMSummary(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
2022-11-01 17:22:07 +00:00
|
|
|
req := request.(*getHostMDMSummaryRequest)
|
|
|
|
resp := getHostMDMSummaryResponse{}
|
|
|
|
var err error
|
|
|
|
|
|
|
|
resp.AggregatedMDMData, err = svc.AggregatedMDMData(ctx, req.TeamID, req.Platform)
|
|
|
|
if err != nil {
|
|
|
|
return getHostMDMSummaryResponse{Err: err}, nil
|
|
|
|
}
|
|
|
|
return resp, nil
|
|
|
|
}
|
|
|
|
|
2021-12-21 12:37:58 +00:00
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Macadmins
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
type getMacadminsDataRequest struct {
|
|
|
|
ID uint `url:"id"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type getMacadminsDataResponse struct {
|
|
|
|
Err error `json:"error,omitempty"`
|
|
|
|
Macadmins *fleet.MacadminsData `json:"macadmins"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r getMacadminsDataResponse) error() error { return r.Err }
|
|
|
|
|
2022-12-27 14:26:59 +00:00
|
|
|
func getMacadminsDataEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
2021-12-21 12:37:58 +00:00
|
|
|
req := request.(*getMacadminsDataRequest)
|
|
|
|
data, err := svc.MacadminsData(ctx, req.ID)
|
|
|
|
if err != nil {
|
|
|
|
return getMacadminsDataResponse{Err: err}, nil
|
|
|
|
}
|
|
|
|
return getMacadminsDataResponse{Macadmins: data}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (svc *Service) MacadminsData(ctx context.Context, id uint) (*fleet.MacadminsData, error) {
|
2023-02-08 23:20:23 +00:00
|
|
|
if !svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceToken) {
|
2022-03-09 21:13:56 +00:00
|
|
|
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2021-12-21 12:37:58 +00:00
|
|
|
|
2022-03-09 21:13:56 +00:00
|
|
|
host, err := svc.ds.HostLite(ctx, id)
|
|
|
|
if err != nil {
|
|
|
|
return nil, ctxerr.Wrap(ctx, err, "find host for macadmins")
|
|
|
|
}
|
2021-12-21 12:37:58 +00:00
|
|
|
|
2022-03-09 21:13:56 +00:00
|
|
|
if err := svc.authz.Authorize(ctx, host, fleet.ActionRead); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2021-12-21 12:37:58 +00:00
|
|
|
}
|
|
|
|
|
2021-12-23 19:57:43 +00:00
|
|
|
var munkiInfo *fleet.HostMunkiInfo
|
2022-09-06 14:34:06 +00:00
|
|
|
switch version, err := svc.ds.GetHostMunkiVersion(ctx, id); {
|
2021-12-23 19:57:43 +00:00
|
|
|
case err != nil && !fleet.IsNotFound(err):
|
2021-12-21 12:37:58 +00:00
|
|
|
return nil, err
|
2021-12-23 19:57:43 +00:00
|
|
|
case err == nil:
|
|
|
|
munkiInfo = &fleet.HostMunkiInfo{Version: version}
|
2021-12-21 12:37:58 +00:00
|
|
|
}
|
|
|
|
|
2021-12-23 19:57:43 +00:00
|
|
|
var mdm *fleet.HostMDM
|
2022-09-06 14:34:06 +00:00
|
|
|
switch hmdm, err := svc.ds.GetHostMDM(ctx, id); {
|
2021-12-23 19:57:43 +00:00
|
|
|
case err != nil && !fleet.IsNotFound(err):
|
2021-12-21 12:37:58 +00:00
|
|
|
return nil, err
|
2021-12-23 19:57:43 +00:00
|
|
|
case err == nil:
|
2022-08-10 19:15:01 +00:00
|
|
|
mdm = hmdm
|
2021-12-21 12:37:58 +00:00
|
|
|
}
|
|
|
|
|
2022-08-29 18:40:16 +00:00
|
|
|
var munkiIssues []*fleet.HostMunkiIssue
|
2022-09-06 14:34:06 +00:00
|
|
|
switch issues, err := svc.ds.GetHostMunkiIssues(ctx, id); {
|
2022-08-29 18:40:16 +00:00
|
|
|
case err != nil:
|
|
|
|
return nil, err
|
|
|
|
case err == nil:
|
|
|
|
munkiIssues = issues
|
|
|
|
}
|
|
|
|
|
|
|
|
if munkiInfo == nil && mdm == nil && len(munkiIssues) == 0 {
|
2021-12-23 19:57:43 +00:00
|
|
|
return nil, nil
|
2021-12-21 12:37:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
data := &fleet.MacadminsData{
|
2022-08-29 18:40:16 +00:00
|
|
|
Munki: munkiInfo,
|
|
|
|
MDM: mdm,
|
|
|
|
MunkiIssues: munkiIssues,
|
2021-12-21 12:37:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return data, nil
|
|
|
|
}
|
2022-01-26 20:55:07 +00:00
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Aggregated Macadmins
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
type getAggregatedMacadminsDataRequest struct {
|
|
|
|
TeamID *uint `query:"team_id,optional"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type getAggregatedMacadminsDataResponse struct {
|
|
|
|
Err error `json:"error,omitempty"`
|
|
|
|
Macadmins *fleet.AggregatedMacadminsData `json:"macadmins"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r getAggregatedMacadminsDataResponse) error() error { return r.Err }
|
|
|
|
|
2022-12-27 14:26:59 +00:00
|
|
|
func getAggregatedMacadminsDataEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
2022-01-26 20:55:07 +00:00
|
|
|
req := request.(*getAggregatedMacadminsDataRequest)
|
|
|
|
data, err := svc.AggregatedMacadminsData(ctx, req.TeamID)
|
|
|
|
if err != nil {
|
|
|
|
return getAggregatedMacadminsDataResponse{Err: err}, nil
|
|
|
|
}
|
|
|
|
return getAggregatedMacadminsDataResponse{Macadmins: data}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (svc *Service) AggregatedMacadminsData(ctx context.Context, teamID *uint) (*fleet.AggregatedMacadminsData, error) {
|
|
|
|
if err := svc.authz.Authorize(ctx, &fleet.Host{TeamID: teamID}, fleet.ActionList); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if teamID != nil {
|
|
|
|
_, err := svc.ds.Team(ctx, *teamID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
agg := &fleet.AggregatedMacadminsData{}
|
|
|
|
|
2022-02-07 17:53:33 +00:00
|
|
|
versions, munkiUpdatedAt, err := svc.ds.AggregatedMunkiVersion(ctx, teamID)
|
2022-01-26 20:55:07 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
agg.MunkiVersions = versions
|
|
|
|
|
2022-08-29 18:40:16 +00:00
|
|
|
issues, munkiIssUpdatedAt, err := svc.ds.AggregatedMunkiIssues(ctx, teamID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
agg.MunkiIssues = issues
|
|
|
|
|
2022-11-01 17:22:07 +00:00
|
|
|
var mdmUpdatedAt, mdmSolutionsUpdatedAt time.Time
|
|
|
|
agg.MDMStatus, mdmUpdatedAt, err = svc.ds.AggregatedMDMStatus(ctx, teamID, "darwin")
|
2022-01-26 20:55:07 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2022-11-01 17:22:07 +00:00
|
|
|
agg.MDMSolutions, mdmSolutionsUpdatedAt, err = svc.ds.AggregatedMDMSolutions(ctx, teamID, "darwin")
|
2022-08-10 19:15:01 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2022-02-07 17:53:33 +00:00
|
|
|
agg.CountsUpdatedAt = munkiUpdatedAt
|
2022-08-29 18:40:16 +00:00
|
|
|
if munkiIssUpdatedAt.After(agg.CountsUpdatedAt) {
|
|
|
|
agg.CountsUpdatedAt = munkiIssUpdatedAt
|
|
|
|
}
|
2022-08-10 19:15:01 +00:00
|
|
|
if mdmUpdatedAt.After(agg.CountsUpdatedAt) {
|
2022-02-07 17:53:33 +00:00
|
|
|
agg.CountsUpdatedAt = mdmUpdatedAt
|
|
|
|
}
|
2022-08-10 19:15:01 +00:00
|
|
|
if mdmSolutionsUpdatedAt.After(agg.CountsUpdatedAt) {
|
|
|
|
agg.CountsUpdatedAt = mdmSolutionsUpdatedAt
|
|
|
|
}
|
2022-02-07 17:53:33 +00:00
|
|
|
|
2022-01-26 20:55:07 +00:00
|
|
|
return agg, nil
|
|
|
|
}
|
2022-03-15 19:14:42 +00:00
|
|
|
|
2022-11-01 17:22:07 +00:00
|
|
|
func (svc *Service) MDMData(ctx context.Context, id uint) (*fleet.HostMDM, error) {
|
|
|
|
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
host, err := svc.ds.HostLite(ctx, id)
|
|
|
|
if err != nil {
|
|
|
|
return nil, ctxerr.Wrap(ctx, err, "find host for MDMData")
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := svc.authz.Authorize(ctx, host, fleet.ActionRead); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2022-12-27 19:22:37 +00:00
|
|
|
hmdm, err := svc.ds.GetHostMDM(ctx, id)
|
|
|
|
switch {
|
2022-11-01 17:22:07 +00:00
|
|
|
case err == nil:
|
2022-12-27 19:22:37 +00:00
|
|
|
return hmdm, nil
|
|
|
|
case fleet.IsNotFound(err):
|
|
|
|
return nil, nil
|
|
|
|
default:
|
|
|
|
return nil, ctxerr.Wrap(ctx, err, "get host mdm")
|
2022-11-01 17:22:07 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (svc *Service) AggregatedMDMData(ctx context.Context, teamID *uint, platform string) (fleet.AggregatedMDMData, error) {
|
|
|
|
if err := svc.authz.Authorize(ctx, &fleet.Host{TeamID: teamID}, fleet.ActionList); err != nil {
|
|
|
|
return fleet.AggregatedMDMData{}, err
|
|
|
|
}
|
|
|
|
|
2022-12-16 21:12:11 +00:00
|
|
|
mdmStatus, mdmStatusUpdatedAt, err := svc.ds.AggregatedMDMStatus(ctx, teamID, platform)
|
2022-11-01 17:22:07 +00:00
|
|
|
if err != nil {
|
|
|
|
return fleet.AggregatedMDMData{}, err
|
|
|
|
}
|
2022-12-16 21:12:11 +00:00
|
|
|
mdmSolutions, mdmSolutionsUpdatedAt, err := svc.ds.AggregatedMDMSolutions(ctx, teamID, platform)
|
2022-11-01 17:22:07 +00:00
|
|
|
if err != nil {
|
|
|
|
return fleet.AggregatedMDMData{}, err
|
|
|
|
}
|
2022-12-16 21:12:11 +00:00
|
|
|
|
|
|
|
countsUpdatedAt := mdmStatusUpdatedAt
|
|
|
|
if mdmStatusUpdatedAt.Before(mdmSolutionsUpdatedAt) {
|
|
|
|
countsUpdatedAt = mdmSolutionsUpdatedAt
|
|
|
|
}
|
|
|
|
|
|
|
|
return fleet.AggregatedMDMData{
|
|
|
|
MDMStatus: mdmStatus,
|
|
|
|
MDMSolutions: mdmSolutions,
|
|
|
|
CountsUpdatedAt: countsUpdatedAt,
|
|
|
|
}, nil
|
2022-11-01 17:22:07 +00:00
|
|
|
}
|
|
|
|
|
2022-03-15 19:14:42 +00:00
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Hosts Report in CSV downloadable file
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
type hostsReportRequest struct {
|
|
|
|
Opts fleet.HostListOptions `url:"host_options"`
|
|
|
|
LabelID *uint `query:"label_id,optional"`
|
|
|
|
Format string `query:"format"`
|
2022-05-10 18:25:53 +00:00
|
|
|
Columns string `query:"columns,optional"`
|
2022-03-15 19:14:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type hostsReportResponse struct {
|
2022-10-14 14:14:18 +00:00
|
|
|
Columns []string `json:"-"` // used to control the generated csv, see the hijackRender method
|
|
|
|
Hosts []*fleet.HostResponse `json:"-"` // they get rendered explicitly, in csv
|
|
|
|
Err error `json:"error,omitempty"`
|
2022-03-15 19:14:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (r hostsReportResponse) error() error { return r.Err }
|
|
|
|
|
|
|
|
func (r hostsReportResponse) hijackRender(ctx context.Context, w http.ResponseWriter) {
|
2022-05-23 19:35:05 +00:00
|
|
|
// post-process the Device Mappings for CSV rendering
|
|
|
|
for _, h := range r.Hosts {
|
|
|
|
if h.DeviceMapping != nil {
|
|
|
|
// return the list of emails, comma-separated, as part of that single CSV field
|
|
|
|
var dms []struct {
|
|
|
|
Email string `json:"email"`
|
|
|
|
}
|
|
|
|
if err := json.Unmarshal(*h.DeviceMapping, &dms); err != nil {
|
|
|
|
// log the error but keep going
|
|
|
|
logging.WithErr(ctx, err)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
var sb strings.Builder
|
|
|
|
for i, dm := range dms {
|
|
|
|
if i > 0 {
|
|
|
|
sb.WriteString(",")
|
|
|
|
}
|
|
|
|
sb.WriteString(dm.Email)
|
|
|
|
}
|
|
|
|
h.CSVDeviceMapping = sb.String()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-10 18:25:53 +00:00
|
|
|
var buf bytes.Buffer
|
|
|
|
if err := gocsv.Marshal(r.Hosts, &buf); err != nil {
|
|
|
|
logging.WithErr(ctx, err)
|
|
|
|
encodeError(ctx, ctxerr.New(ctx, "failed to generate CSV file"), w)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
returnAll := len(r.Columns) == 0
|
|
|
|
|
|
|
|
var outRows [][]string
|
|
|
|
if !returnAll {
|
|
|
|
// read back the CSV to filter out any unwanted columns
|
|
|
|
recs, err := csv.NewReader(&buf).ReadAll()
|
|
|
|
if err != nil {
|
|
|
|
logging.WithErr(ctx, err)
|
|
|
|
encodeError(ctx, ctxerr.New(ctx, "failed to generate CSV file"), w)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(recs) > 0 {
|
|
|
|
// map the header names to their field index
|
|
|
|
hdrs := make(map[string]int, len(recs))
|
|
|
|
for i, hdr := range recs[0] {
|
|
|
|
hdrs[hdr] = i
|
|
|
|
}
|
|
|
|
|
|
|
|
outRows = make([][]string, len(recs))
|
|
|
|
for i, rec := range recs {
|
|
|
|
for _, col := range r.Columns {
|
|
|
|
colIx, ok := hdrs[col]
|
|
|
|
if !ok {
|
|
|
|
// invalid column name - it would be nice to catch this in the
|
|
|
|
// endpoint before processing the results, but it would require
|
|
|
|
// duplicating the list of columns from the Host's struct tags to a
|
|
|
|
// map and keep this in sync, for what is essentially a programmer
|
|
|
|
// mistake that should be caught and corrected early.
|
2022-09-19 17:53:44 +00:00
|
|
|
encodeError(ctx, &fleet.BadRequestError{Message: fmt.Sprintf("invalid column name: %q", col)}, w)
|
2022-05-10 18:25:53 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
outRows[i] = append(outRows[i], rec[colIx])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-15 19:14:42 +00:00
|
|
|
w.Header().Add("Content-Disposition", fmt.Sprintf(`attachment; filename="Hosts %s.csv"`, time.Now().Format("2006-01-02")))
|
|
|
|
w.Header().Set("Content-Type", "text/csv")
|
2022-10-12 13:19:21 +00:00
|
|
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
2022-03-15 19:14:42 +00:00
|
|
|
w.WriteHeader(http.StatusOK)
|
2022-05-10 18:25:53 +00:00
|
|
|
|
|
|
|
var err error
|
|
|
|
if returnAll {
|
|
|
|
_, err = io.Copy(w, &buf)
|
|
|
|
} else {
|
|
|
|
err = csv.NewWriter(w).WriteAll(outRows)
|
|
|
|
}
|
|
|
|
if err != nil {
|
2022-03-15 19:14:42 +00:00
|
|
|
logging.WithErr(ctx, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-27 14:26:59 +00:00
|
|
|
func hostsReportEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
2022-03-15 19:14:42 +00:00
|
|
|
req := request.(*hostsReportRequest)
|
|
|
|
|
|
|
|
// for now, only csv format is allowed
|
|
|
|
if req.Format != "csv" {
|
|
|
|
// prevent returning an "unauthorized" error, we want that specific error
|
2023-02-08 23:20:23 +00:00
|
|
|
if az, ok := authzctx.FromContext(ctx); ok {
|
2022-03-15 19:14:42 +00:00
|
|
|
az.SetChecked()
|
|
|
|
}
|
|
|
|
err := ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("format", "unsupported or unspecified report format").
|
|
|
|
WithStatus(http.StatusUnsupportedMediaType))
|
|
|
|
return hostsReportResponse{Err: err}, nil
|
|
|
|
}
|
|
|
|
|
2022-05-23 19:35:05 +00:00
|
|
|
req.Opts.DisableFailingPolicies = false
|
2022-03-15 19:14:42 +00:00
|
|
|
req.Opts.AdditionalFilters = nil
|
2022-03-16 13:01:52 +00:00
|
|
|
req.Opts.Page = 0
|
|
|
|
req.Opts.PerPage = 0 // explicitly disable any limit, we want all matching hosts
|
|
|
|
req.Opts.After = ""
|
2022-05-23 19:35:05 +00:00
|
|
|
req.Opts.DeviceMapping = false
|
|
|
|
|
|
|
|
rawCols := strings.Split(req.Columns, ",")
|
|
|
|
var cols []string
|
|
|
|
for _, rawCol := range rawCols {
|
|
|
|
if rawCol = strings.TrimSpace(rawCol); rawCol != "" {
|
|
|
|
cols = append(cols, rawCol)
|
|
|
|
if rawCol == "device_mapping" {
|
|
|
|
req.Opts.DeviceMapping = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if len(cols) == 0 {
|
|
|
|
// enable device_mapping retrieval, as no column means all columns
|
|
|
|
req.Opts.DeviceMapping = true
|
|
|
|
}
|
2022-03-15 19:14:42 +00:00
|
|
|
|
|
|
|
var (
|
|
|
|
hosts []*fleet.Host
|
|
|
|
err error
|
|
|
|
)
|
|
|
|
|
|
|
|
if req.LabelID == nil {
|
|
|
|
hosts, err = svc.ListHosts(ctx, req.Opts)
|
|
|
|
} else {
|
|
|
|
hosts, err = svc.ListHostsInLabel(ctx, *req.LabelID, req.Opts)
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return hostsReportResponse{Err: err}, nil
|
|
|
|
}
|
2022-05-10 18:25:53 +00:00
|
|
|
|
2022-10-14 14:14:18 +00:00
|
|
|
hostResps := make([]*fleet.HostResponse, len(hosts))
|
2022-05-10 18:25:53 +00:00
|
|
|
for i, h := range hosts {
|
2022-10-14 14:14:18 +00:00
|
|
|
hr, err := fleet.HostResponseForHost(ctx, svc, h)
|
2022-05-10 18:25:53 +00:00
|
|
|
if err != nil {
|
|
|
|
return hostsReportResponse{Err: err}, nil
|
|
|
|
}
|
|
|
|
hostResps[i] = hr
|
|
|
|
}
|
|
|
|
return hostsReportResponse{Columns: cols, Hosts: hostResps}, nil
|
2022-03-15 19:14:42 +00:00
|
|
|
}
|
2022-03-28 15:15:45 +00:00
|
|
|
|
|
|
|
type osVersionsRequest struct {
|
2022-08-22 19:34:00 +00:00
|
|
|
TeamID *uint `query:"team_id,optional"`
|
|
|
|
Platform *string `query:"platform,optional"`
|
|
|
|
Name *string `query:"os_name,optional"`
|
|
|
|
Version *string `query:"os_name,optional"`
|
2022-03-28 15:15:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type osVersionsResponse struct {
|
2022-04-18 21:19:58 +00:00
|
|
|
CountsUpdatedAt *time.Time `json:"counts_updated_at"`
|
|
|
|
OSVersions []fleet.OSVersion `json:"os_versions"`
|
2022-03-28 15:15:45 +00:00
|
|
|
Err error `json:"error,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r osVersionsResponse) error() error { return r.Err }
|
|
|
|
|
2022-12-27 14:26:59 +00:00
|
|
|
func osVersionsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
2022-03-28 15:15:45 +00:00
|
|
|
req := request.(*osVersionsRequest)
|
|
|
|
|
2022-08-22 19:34:00 +00:00
|
|
|
osVersions, err := svc.OSVersions(ctx, req.TeamID, req.Platform, req.Name, req.Version)
|
2022-03-28 15:15:45 +00:00
|
|
|
if err != nil {
|
|
|
|
return &osVersionsResponse{Err: err}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return &osVersionsResponse{
|
|
|
|
CountsUpdatedAt: &osVersions.CountsUpdatedAt,
|
|
|
|
OSVersions: osVersions.OSVersions,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2022-08-22 19:34:00 +00:00
|
|
|
func (svc *Service) OSVersions(ctx context.Context, teamID *uint, platform *string, name *string, version *string) (*fleet.OSVersions, error) {
|
2022-03-28 15:15:45 +00:00
|
|
|
if err := svc.authz.Authorize(ctx, &fleet.Host{TeamID: teamID}, fleet.ActionList); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2022-08-22 19:34:00 +00:00
|
|
|
if name != nil && version == nil {
|
2022-09-19 17:53:44 +00:00
|
|
|
return nil, &fleet.BadRequestError{Message: "Cannot specify os_name without os_version"}
|
2022-08-22 19:34:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if name == nil && version != nil {
|
2022-09-19 17:53:44 +00:00
|
|
|
return nil, &fleet.BadRequestError{Message: "Cannot specify os_version without os_name"}
|
2022-08-22 19:34:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
osVersions, err := svc.ds.OSVersions(ctx, teamID, platform, name, version)
|
2022-04-18 21:19:58 +00:00
|
|
|
if err != nil && fleet.IsNotFound(err) {
|
|
|
|
// differentiate case where team was added after UpdateOSVersions last ran
|
|
|
|
if teamID != nil {
|
|
|
|
// most of the time, team should exist so checking here saves unnecessary db calls
|
|
|
|
_, err := svc.ds.Team(ctx, *teamID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// if team exists but stats have not yet been gathered, return empty JSON array
|
|
|
|
osVersions = &fleet.OSVersions{}
|
|
|
|
} else if err != nil {
|
2022-03-28 15:15:45 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return osVersions, nil
|
|
|
|
}
|
2023-02-08 23:20:23 +00:00
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Encryption Key
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
type getHostEncryptionKeyRequest struct {
|
|
|
|
ID uint `url:"id"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type getHostEncryptionKeyResponse struct {
|
|
|
|
Err error `json:"error,omitempty"`
|
|
|
|
EncryptionKey *fleet.HostDiskEncryptionKey `json:"encryption_key,omitempty"`
|
|
|
|
HostID uint `json:"host_id,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r getHostEncryptionKeyResponse) error() error { return r.Err }
|
|
|
|
|
|
|
|
func getHostEncryptionKey(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
|
|
|
req := request.(*getHostEncryptionKeyRequest)
|
|
|
|
key, err := svc.HostEncryptionKey(ctx, req.ID)
|
|
|
|
if err != nil {
|
|
|
|
return getHostEncryptionKeyResponse{Err: err}, nil
|
|
|
|
}
|
|
|
|
return getHostEncryptionKeyResponse{EncryptionKey: key, HostID: req.ID}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (svc *Service) HostEncryptionKey(ctx context.Context, id uint) (*fleet.HostDiskEncryptionKey, error) {
|
|
|
|
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
host, err := svc.ds.HostLite(ctx, id)
|
|
|
|
if err != nil {
|
|
|
|
return nil, ctxerr.Wrap(ctx, err, "getting host encryption key")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Permissions to read encryption keys are exactly the same
|
|
|
|
// as the ones required to read hosts.
|
|
|
|
if err := svc.authz.Authorize(ctx, host, fleet.ActionRead); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
key, err := svc.ds.GetHostDiskEncryptionKey(ctx, id)
|
|
|
|
if err != nil {
|
|
|
|
return nil, ctxerr.Wrap(ctx, err, "getting host encryption key")
|
|
|
|
}
|
|
|
|
|
|
|
|
if key.Decryptable == nil || !*key.Decryptable {
|
|
|
|
return nil, ctxerr.Wrap(ctx, notFoundError{}, "getting host encryption key")
|
|
|
|
}
|
|
|
|
|
|
|
|
cert, _, _, err := svc.config.MDM.AppleSCEP()
|
|
|
|
if err != nil {
|
|
|
|
return nil, ctxerr.Wrap(ctx, err, "getting host encryption key")
|
|
|
|
}
|
|
|
|
|
|
|
|
decryptedKey, err := apple_mdm.DecryptBase64CMS(key.Base64Encrypted, cert.Leaf, cert.PrivateKey)
|
|
|
|
if err != nil {
|
|
|
|
return nil, ctxerr.Wrap(ctx, err, "getting host encryption key")
|
|
|
|
}
|
|
|
|
|
|
|
|
key.DecryptedValue = string(decryptedKey)
|
|
|
|
|
|
|
|
err = svc.ds.NewActivity(
|
|
|
|
ctx,
|
|
|
|
authz.UserFromContext(ctx),
|
|
|
|
fleet.ActivityTypeReadHostDiskEncryptionKey{
|
|
|
|
HostID: host.ID,
|
|
|
|
HostDisplayName: host.DisplayName(),
|
|
|
|
},
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, ctxerr.Wrap(ctx, err, "create read host disk encryption key activity")
|
|
|
|
}
|
|
|
|
|
|
|
|
return key, nil
|
|
|
|
}
|