mirror of
https://github.com/fleetdm/fleet
synced 2026-05-06 06:48:54 +00:00
For #32542. - [X] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. - [X] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) ## Testing - [X] Added/updated automated tests - [X] Where appropriate, [automated tests simulate multiple hosts and test for host isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing) (updates to one hosts's records do not affect another) - [X] QA'd all new/changed functionality manually
287 lines
10 KiB
Go
287 lines
10 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"path/filepath"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/docker/go-units"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
"github.com/fleetdm/fleet/v4/server/ptr"
|
|
)
|
|
|
|
type putSetupExperienceSoftwareRequest struct {
|
|
Platform string `json:"platform"`
|
|
TeamID uint `json:"team_id"`
|
|
TitleIDs []uint `json:"software_title_ids"`
|
|
}
|
|
|
|
func (r *putSetupExperienceSoftwareRequest) ValidateRequest() error {
|
|
return validateSetupExperiencePlatform(r.Platform)
|
|
}
|
|
|
|
type putSetupExperienceSoftwareResponse struct {
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r putSetupExperienceSoftwareResponse) Error() error { return r.Err }
|
|
|
|
func putSetupExperienceSoftware(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*putSetupExperienceSoftwareRequest)
|
|
platform := transformPlatformForSetupExperience(req.Platform)
|
|
err := svc.SetSetupExperienceSoftware(ctx, platform, req.TeamID, req.TitleIDs)
|
|
if err != nil {
|
|
return &putSetupExperienceSoftwareResponse{Err: err}, nil
|
|
}
|
|
return &putSetupExperienceSoftwareResponse{}, nil
|
|
}
|
|
|
|
func (svc *Service) SetSetupExperienceSoftware(ctx context.Context, platform string, teamID uint, titleIDs []uint) error {
|
|
// skipauth: No authorization check needed due to implementation returning
|
|
// only license error.
|
|
svc.authz.SkipAuthorization(ctx)
|
|
|
|
return fleet.ErrMissingLicense
|
|
}
|
|
|
|
type getSetupExperienceSoftwareRequest struct {
|
|
Platform string `query:"platform,optional"`
|
|
fleet.ListOptions
|
|
TeamID uint `query:"team_id"`
|
|
}
|
|
|
|
func (r *getSetupExperienceSoftwareRequest) ValidateRequest() error {
|
|
return validateSetupExperiencePlatform(r.Platform)
|
|
}
|
|
|
|
type getSetupExperienceSoftwareResponse struct {
|
|
SoftwareTitles []fleet.SoftwareTitleListResult `json:"software_titles"`
|
|
Count int `json:"count"`
|
|
Meta *fleet.PaginationMetadata `json:"meta"`
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r getSetupExperienceSoftwareResponse) Error() error { return r.Err }
|
|
|
|
func getSetupExperienceSoftware(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*getSetupExperienceSoftwareRequest)
|
|
platform := transformPlatformForSetupExperience(req.Platform)
|
|
titles, count, meta, err := svc.ListSetupExperienceSoftware(ctx, platform, req.TeamID, req.ListOptions)
|
|
if err != nil {
|
|
return &getSetupExperienceSoftwareResponse{Err: err}, nil
|
|
}
|
|
return &getSetupExperienceSoftwareResponse{SoftwareTitles: titles, Count: count, Meta: meta}, nil
|
|
}
|
|
|
|
func (svc *Service) ListSetupExperienceSoftware(ctx context.Context, platform string, teamID uint, opts fleet.ListOptions) ([]fleet.SoftwareTitleListResult, int, *fleet.PaginationMetadata, error) {
|
|
// skipauth: No authorization check needed due to implementation returning
|
|
// only license error.
|
|
svc.authz.SkipAuthorization(ctx)
|
|
|
|
return nil, 0, nil, fleet.ErrMissingLicense
|
|
}
|
|
|
|
type getSetupExperienceScriptRequest struct {
|
|
TeamID *uint `query:"team_id,optional"`
|
|
Alt string `query:"alt,optional"`
|
|
}
|
|
|
|
type getSetupExperienceScriptResponse struct {
|
|
*fleet.Script
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r getSetupExperienceScriptResponse) Error() error { return r.Err }
|
|
|
|
func getSetupExperienceScriptEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*getSetupExperienceScriptRequest)
|
|
downloadRequested := req.Alt == "media"
|
|
// // TODO: do we want to allow end users to specify team_id=0? if so, we'll need convert it to nil here so that we can
|
|
// // use it in the auth layer where team_id=0 is not allowed?
|
|
script, content, err := svc.GetSetupExperienceScript(ctx, req.TeamID, downloadRequested)
|
|
if err != nil {
|
|
return getSetupExperienceScriptResponse{Err: err}, nil
|
|
}
|
|
|
|
if downloadRequested {
|
|
return downloadFileResponse{
|
|
content: content,
|
|
filename: fmt.Sprintf("%s %s", time.Now().Format(time.DateOnly), script.Name),
|
|
}, nil
|
|
}
|
|
|
|
return getSetupExperienceScriptResponse{Script: script}, nil
|
|
}
|
|
|
|
func (svc *Service) GetSetupExperienceScript(ctx context.Context, teamID *uint, withContent bool) (*fleet.Script, []byte, error) {
|
|
// skipauth: No authorization check needed due to implementation returning
|
|
// only license error.
|
|
svc.authz.SkipAuthorization(ctx)
|
|
|
|
return nil, nil, fleet.ErrMissingLicense
|
|
}
|
|
|
|
type setSetupExperienceScriptRequest struct {
|
|
TeamID *uint
|
|
Script *multipart.FileHeader
|
|
}
|
|
|
|
func (setSetupExperienceScriptRequest) DecodeRequest(ctx context.Context, r *http.Request) (interface{}, error) {
|
|
var decoded setSetupExperienceScriptRequest
|
|
|
|
err := r.ParseMultipartForm(512 * units.MiB) // same in-memory size as for other multipart requests we have
|
|
if err != nil {
|
|
return nil, &fleet.BadRequestError{
|
|
Message: "failed to parse multipart form",
|
|
InternalErr: err,
|
|
}
|
|
}
|
|
|
|
val := r.MultipartForm.Value["team_id"]
|
|
if len(val) > 0 {
|
|
teamID, err := strconv.ParseUint(val[0], 10, 64)
|
|
if err != nil {
|
|
return nil, &fleet.BadRequestError{Message: fmt.Sprintf("failed to decode team_id in multipart form: %s", err.Error())}
|
|
}
|
|
// // TODO: do we want to allow end users to specify team_id=0? if so, we'll need to convert it to nil here so that we can
|
|
// // use it in the auth layer where team_id=0 is not allowed?
|
|
decoded.TeamID = ptr.Uint(uint(teamID))
|
|
}
|
|
|
|
fhs, ok := r.MultipartForm.File["script"]
|
|
if !ok || len(fhs) < 1 {
|
|
return nil, &fleet.BadRequestError{Message: "no file headers for script"}
|
|
}
|
|
decoded.Script = fhs[0]
|
|
|
|
return &decoded, nil
|
|
}
|
|
|
|
type setSetupExperienceScriptResponse struct {
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r setSetupExperienceScriptResponse) Error() error { return r.Err }
|
|
|
|
func setSetupExperienceScriptEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*setSetupExperienceScriptRequest)
|
|
|
|
scriptFile, err := req.Script.Open()
|
|
if err != nil {
|
|
return setSetupExperienceScriptResponse{Err: err}, nil
|
|
}
|
|
defer scriptFile.Close()
|
|
|
|
if err := svc.SetSetupExperienceScript(ctx, req.TeamID, filepath.Base(req.Script.Filename), scriptFile); err != nil {
|
|
return setSetupExperienceScriptResponse{Err: err}, nil
|
|
}
|
|
|
|
return setSetupExperienceScriptResponse{}, nil
|
|
}
|
|
|
|
func (svc *Service) SetSetupExperienceScript(ctx context.Context, teamID *uint, name string, r io.Reader) error {
|
|
// skipauth: No authorization check needed due to implementation returning
|
|
// only license error.
|
|
svc.authz.SkipAuthorization(ctx)
|
|
|
|
return fleet.ErrMissingLicense
|
|
}
|
|
|
|
type deleteSetupExperienceScriptRequest struct {
|
|
TeamID *uint `query:"team_id,optional"`
|
|
}
|
|
|
|
type deleteSetupExperienceScriptResponse struct {
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r deleteSetupExperienceScriptResponse) Error() error { return r.Err }
|
|
|
|
func deleteSetupExperienceScriptEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*deleteSetupExperienceScriptRequest)
|
|
// // TODO: do we want to allow end users to specify team_id=0? if so, we'll need convert it to nil here so that we can
|
|
// // use it in the auth layer where team_id=0 is not allowed?
|
|
if err := svc.DeleteSetupExperienceScript(ctx, req.TeamID); err != nil {
|
|
return deleteSetupExperienceScriptResponse{Err: err}, nil
|
|
}
|
|
|
|
return deleteSetupExperienceScriptResponse{}, nil
|
|
}
|
|
|
|
func (svc *Service) DeleteSetupExperienceScript(ctx context.Context, teamID *uint) error {
|
|
// skipauth: No authorization check needed due to implementation returning
|
|
// only license error.
|
|
svc.authz.SkipAuthorization(ctx)
|
|
|
|
return fleet.ErrMissingLicense
|
|
}
|
|
|
|
func (svc *Service) SetupExperienceNextStep(ctx context.Context, host *fleet.Host) (bool, error) {
|
|
// skipauth: No authorization check needed due to implementation returning
|
|
// only license error.
|
|
svc.authz.SkipAuthorization(ctx)
|
|
|
|
return false, fleet.ErrMissingLicense
|
|
}
|
|
|
|
// maybeUpdateSetupExperienceStatus attempts to update the status of a setup experience result in
|
|
// the database. If the given result is of a supported type (namely SetupExperienceScriptResult,
|
|
// SetupExperienceSoftwareInstallResult, and SetupExperienceVPPInstallResult), it returns a boolean
|
|
// indicating whether the datastore was updated and an error if one occurred. If the result is not of a
|
|
// supported type, it returns false and an error indicated that the type is not supported.
|
|
// If the skipPending parameter is true, the datastore will only be updated if the given result
|
|
// status is not pending.
|
|
func maybeUpdateSetupExperienceStatus(ctx context.Context, ds fleet.Datastore, result interface{}, requireTerminalStatus bool) (bool, error) {
|
|
switch v := result.(type) {
|
|
case fleet.SetupExperienceScriptResult:
|
|
status := v.SetupExperienceStatus()
|
|
if !status.IsValid() {
|
|
return false, fmt.Errorf("invalid status: %s", status)
|
|
} else if requireTerminalStatus && !status.IsTerminalStatus() {
|
|
return false, nil
|
|
}
|
|
return ds.MaybeUpdateSetupExperienceScriptStatus(ctx, v.HostUUID, v.ExecutionID, status)
|
|
|
|
case fleet.SetupExperienceSoftwareInstallResult:
|
|
status := v.SetupExperienceStatus()
|
|
if !status.IsValid() {
|
|
return false, fmt.Errorf("invalid status: %s", status)
|
|
} else if requireTerminalStatus && !status.IsTerminalStatus() {
|
|
return false, nil
|
|
}
|
|
return ds.MaybeUpdateSetupExperienceSoftwareInstallStatus(ctx, v.HostUUID, v.ExecutionID, status)
|
|
|
|
case fleet.SetupExperienceVPPInstallResult:
|
|
// NOTE: this case is also implemented in the CommandAndReportResults method of
|
|
// MDMAppleCheckinAndCommandService
|
|
status := v.SetupExperienceStatus()
|
|
if !status.IsValid() {
|
|
return false, fmt.Errorf("invalid status: %s", status)
|
|
} else if requireTerminalStatus && !status.IsTerminalStatus() {
|
|
return false, nil
|
|
}
|
|
return ds.MaybeUpdateSetupExperienceVPPStatus(ctx, v.HostUUID, v.CommandUUID, status)
|
|
|
|
default:
|
|
return false, fmt.Errorf("unsupported result type: %T", result)
|
|
}
|
|
}
|
|
|
|
func validateSetupExperiencePlatform(platform string) error {
|
|
if platform != "" && platform != "macos" && platform != "windows" && platform != "linux" {
|
|
return badRequestf("platform %q unsupported, platform must be \"macos\", \"windows\", or \"linux\"", platform)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func transformPlatformForSetupExperience(platform string) string {
|
|
if platform == "" || platform == "macos" {
|
|
return "darwin"
|
|
}
|
|
return platform
|
|
}
|