package service import ( "context" "crypto/x509" "encoding/json" "errors" "fmt" "io" "mime/multipart" "net" "net/http" "net/url" "strconv" authzctx "github.com/fleetdm/fleet/v4/server/contexts/authz" "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" hostctx "github.com/fleetdm/fleet/v4/server/contexts/host" "github.com/fleetdm/fleet/v4/server/contexts/installersize" "github.com/fleetdm/fleet/v4/server/contexts/logging" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/platform/endpointer" platform_http "github.com/fleetdm/fleet/v4/server/platform/http" "github.com/fleetdm/fleet/v4/server/ptr" ) type uploadSoftwareInstallerRequest struct { File *multipart.FileHeader TeamID *uint InstallScript string PreInstallQuery string PostInstallScript string SelfService bool UninstallScript string LabelsIncludeAny []string LabelsExcludeAny []string AutomaticInstall bool } type updateSoftwareInstallerRequest struct { TitleID uint `url:"id"` File *multipart.FileHeader TeamID *uint InstallScript *string PreInstallQuery *string PostInstallScript *string UninstallScript *string SelfService *bool LabelsIncludeAny []string LabelsExcludeAny []string Categories []string DisplayName *string } type uploadSoftwareInstallerResponse struct { SoftwarePackage *fleet.SoftwareInstaller `json:"software_package,omitempty"` Err error `json:"error,omitempty"` } // TODO: We parse the whole body before running svc.authz.Authorize. // An authenticated but unauthorized user could abuse this. func (updateSoftwareInstallerRequest) DecodeRequest(ctx context.Context, r *http.Request) (interface{}, error) { decoded := updateSoftwareInstallerRequest{} // populate software title ID since we're overriding the decoder that would do it for us titleID, err := uint32FromRequest(r, "id") if err != nil { return nil, endpointer.BadRequestErr("IntFromRequest", err) } decoded.TitleID = uint(titleID) maxInstallerSize := installersize.FromContext(ctx) err = r.ParseMultipartForm(platform_http.MaxMultipartFormSize) if err != nil { var mbe *http.MaxBytesError if errors.As(err, &mbe) { return nil, &fleet.BadRequestError{ Message: fmt.Sprintf("The maximum file size is %s.", installersize.Human(maxInstallerSize)), InternalErr: err, } } var nerr net.Error if errors.As(err, &nerr) && nerr.Timeout() { return nil, fleet.NewUserMessageError( ctxerr.New(ctx, "Couldn't add. Please ensure your internet connection speed is sufficient and stable."), http.StatusRequestTimeout, ) } return nil, &fleet.BadRequestError{ Message: "failed to parse multipart form: " + err.Error(), InternalErr: err, } } // unlike for uploadSoftwareInstallerRequest, every field is optional, including the file upload if r.MultipartForm.File["software"] != nil || len(r.MultipartForm.File["software"]) > 0 { decoded.File = r.MultipartForm.File["software"][0] if decoded.File.Size > maxInstallerSize { // Should never happen here since the request's body is limited to the maximum size. return nil, &fleet.BadRequestError{ Message: fmt.Sprintf("The maximum file size is %s.", installersize.Human(maxInstallerSize)), } } } // default is no team val, ok := r.MultipartForm.Value["team_id"] if ok { teamID, err := strconv.ParseUint(val[0], 10, 32) if err != nil { return nil, &fleet.BadRequestError{Message: fmt.Sprintf("Invalid team_id: %s", val[0])} } decoded.TeamID = ptr.Uint(uint(teamID)) } installScriptMultipart, ok := r.MultipartForm.Value["install_script"] if ok && len(installScriptMultipart) > 0 { decoded.InstallScript = &installScriptMultipart[0] } preinstallQueryMultipart, ok := r.MultipartForm.Value["pre_install_query"] if ok && len(preinstallQueryMultipart) > 0 { decoded.PreInstallQuery = &preinstallQueryMultipart[0] } postInstallScriptMultipart, ok := r.MultipartForm.Value["post_install_script"] if ok && len(postInstallScriptMultipart) > 0 { decoded.PostInstallScript = &postInstallScriptMultipart[0] } uninstallScriptMultipart, ok := r.MultipartForm.Value["uninstall_script"] if ok && len(uninstallScriptMultipart) > 0 { decoded.UninstallScript = &uninstallScriptMultipart[0] } val, ok = r.MultipartForm.Value["self_service"] if ok && len(val) > 0 && val[0] != "" { parsed, err := strconv.ParseBool(val[0]) if err != nil { return nil, &fleet.BadRequestError{Message: fmt.Sprintf("failed to decode self_service bool in multipart form: %s", err.Error())} } decoded.SelfService = &parsed } // decode labels and categories var inclAny, exclAny, categories []string var existsInclAny, existsExclAny, existsCategories bool inclAny, existsInclAny = r.MultipartForm.Value[string(fleet.LabelsIncludeAny)] switch { case !existsInclAny: decoded.LabelsIncludeAny = nil case len(inclAny) == 1 && inclAny[0] == "": decoded.LabelsIncludeAny = []string{} default: decoded.LabelsIncludeAny = inclAny } exclAny, existsExclAny = r.MultipartForm.Value[string(fleet.LabelsExcludeAny)] switch { case !existsExclAny: decoded.LabelsExcludeAny = nil case len(exclAny) == 1 && exclAny[0] == "": decoded.LabelsExcludeAny = []string{} default: decoded.LabelsExcludeAny = exclAny } categories, existsCategories = r.MultipartForm.Value["categories"] switch { case !existsCategories: decoded.Categories = nil case len(categories) == 1 && categories[0] == "": decoded.Categories = []string{} default: decoded.Categories = categories } displayNameMultiPart, existsDisplayName := r.MultipartForm.Value["display_name"] if existsDisplayName && len(displayNameMultiPart) > 0 { decoded.DisplayName = ptr.String(displayNameMultiPart[0]) if len(*decoded.DisplayName) > fleet.SoftwareTitleDisplayNameMaxLength { return nil, &fleet.BadRequestError{ Message: "The maximum display name length is 255 characters.", } } } // Check if scripts are base64 encoded (to bypass WAF rules that block script patterns) if isScriptsEncoded(r) { if decoded.InstallScript != nil { decodedScript, err := decodeBase64Script(*decoded.InstallScript) if err != nil { return nil, &fleet.BadRequestError{Message: "invalid base64 encoding for install_script"} } decoded.InstallScript = &decodedScript } if decoded.UninstallScript != nil { decodedScript, err := decodeBase64Script(*decoded.UninstallScript) if err != nil { return nil, &fleet.BadRequestError{Message: "invalid base64 encoding for uninstall_script"} } decoded.UninstallScript = &decodedScript } if decoded.PreInstallQuery != nil { decodedScript, err := decodeBase64Script(*decoded.PreInstallQuery) if err != nil { return nil, &fleet.BadRequestError{Message: "invalid base64 encoding for pre_install_query"} } decoded.PreInstallQuery = &decodedScript } if decoded.PostInstallScript != nil { decodedScript, err := decodeBase64Script(*decoded.PostInstallScript) if err != nil { return nil, &fleet.BadRequestError{Message: "invalid base64 encoding for post_install_script"} } decoded.PostInstallScript = &decodedScript } } return &decoded, nil } func updateSoftwareInstallerEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*updateSoftwareInstallerRequest) payload := &fleet.UpdateSoftwareInstallerPayload{ TitleID: req.TitleID, TeamID: req.TeamID, InstallScript: req.InstallScript, PreInstallQuery: req.PreInstallQuery, PostInstallScript: req.PostInstallScript, UninstallScript: req.UninstallScript, SelfService: req.SelfService, LabelsIncludeAny: req.LabelsIncludeAny, LabelsExcludeAny: req.LabelsExcludeAny, Categories: req.Categories, DisplayName: req.DisplayName, } if req.File != nil { ff, err := req.File.Open() if err != nil { return uploadSoftwareInstallerResponse{Err: err}, nil } defer ff.Close() tfr, err := fleet.NewTempFileReader(ff, nil) if err != nil { return uploadSoftwareInstallerResponse{Err: err}, nil } defer tfr.Close() payload.InstallerFile = tfr payload.Filename = req.File.Filename } installer, err := svc.UpdateSoftwareInstaller(ctx, payload) if err != nil { return uploadSoftwareInstallerResponse{Err: err}, nil } return getSoftwareInstallerResponse{SoftwareInstaller: installer}, nil } func (svc *Service) UpdateSoftwareInstaller(ctx context.Context, payload *fleet.UpdateSoftwareInstallerPayload) (*fleet.SoftwareInstaller, error) { // skipauth: No authorization check needed due to implementation returning // only license error. svc.authz.SkipAuthorization(ctx) return nil, fleet.ErrMissingLicense } // TODO: We parse the whole body before running svc.authz.Authorize. // An authenticated but unauthorized user could abuse this. func (uploadSoftwareInstallerRequest) DecodeRequest(ctx context.Context, r *http.Request) (interface{}, error) { decoded := uploadSoftwareInstallerRequest{} maxInstallerSize := installersize.FromContext(ctx) err := r.ParseMultipartForm(platform_http.MaxMultipartFormSize) if err != nil { var mbe *http.MaxBytesError if errors.As(err, &mbe) { return nil, &fleet.BadRequestError{ Message: fmt.Sprintf("The maximum file size is %s.", installersize.Human(maxInstallerSize)), InternalErr: err, } } var nerr net.Error if errors.As(err, &nerr) && nerr.Timeout() { return nil, fleet.NewUserMessageError( ctxerr.New(ctx, "Couldn't add. Please ensure your internet connection speed is sufficient and stable."), http.StatusRequestTimeout, ) } return nil, &fleet.BadRequestError{ Message: "failed to parse multipart form: " + err.Error(), InternalErr: err, } } if r.MultipartForm.File["software"] == nil || len(r.MultipartForm.File["software"]) == 0 { return nil, &fleet.BadRequestError{ Message: "software multipart field is required", InternalErr: err, } } decoded.File = r.MultipartForm.File["software"][0] if decoded.File.Size > maxInstallerSize { // Should never happen here since the request's body is limited to the // maximum size. return nil, &fleet.BadRequestError{ Message: fmt.Sprintf("The maximum file size is %s.", installersize.Human(maxInstallerSize)), } } // default is no team val, ok := r.MultipartForm.Value["team_id"] if ok { teamID, err := strconv.ParseUint(val[0], 10, 32) if err != nil { return nil, &fleet.BadRequestError{Message: fmt.Sprintf("Invalid team_id: %s", val[0])} } decoded.TeamID = ptr.Uint(uint(teamID)) } val, ok = r.MultipartForm.Value["install_script"] if ok && len(val) > 0 { decoded.InstallScript = val[0] } val, ok = r.MultipartForm.Value["uninstall_script"] if ok && len(val) > 0 { decoded.UninstallScript = val[0] } val, ok = r.MultipartForm.Value["pre_install_query"] if ok && len(val) > 0 { decoded.PreInstallQuery = val[0] } val, ok = r.MultipartForm.Value["post_install_script"] if ok && len(val) > 0 { decoded.PostInstallScript = val[0] } val, ok = r.MultipartForm.Value["self_service"] if ok && len(val) > 0 && val[0] != "" { parsed, err := strconv.ParseBool(val[0]) if err != nil { return nil, &fleet.BadRequestError{Message: fmt.Sprintf("failed to decode self_service bool in multipart form: %s", err.Error())} } decoded.SelfService = parsed } // decode labels var inclAny, exclAny []string var existsInclAny, existsExclAny bool inclAny, existsInclAny = r.MultipartForm.Value[string(fleet.LabelsIncludeAny)] switch { case !existsInclAny: decoded.LabelsIncludeAny = nil case len(inclAny) == 1 && inclAny[0] == "": decoded.LabelsIncludeAny = []string{} default: decoded.LabelsIncludeAny = inclAny } exclAny, existsExclAny = r.MultipartForm.Value[string(fleet.LabelsExcludeAny)] switch { case !existsExclAny: decoded.LabelsExcludeAny = nil case len(exclAny) == 1 && exclAny[0] == "": decoded.LabelsExcludeAny = []string{} default: decoded.LabelsExcludeAny = exclAny } val, ok = r.MultipartForm.Value["automatic_install"] if ok && len(val) > 0 && val[0] != "" { parsed, err := strconv.ParseBool(val[0]) if err != nil { return nil, &fleet.BadRequestError{Message: fmt.Sprintf("failed to decode automatic_install bool in multipart form: %s", err.Error())} } decoded.AutomaticInstall = parsed } // Check if scripts are base64 encoded (to bypass WAF rules that block script patterns) if isScriptsEncoded(r) { var err error if decoded.InstallScript, err = decodeBase64Script(decoded.InstallScript); err != nil { return nil, &fleet.BadRequestError{Message: "invalid base64 encoding for install_script"} } if decoded.UninstallScript, err = decodeBase64Script(decoded.UninstallScript); err != nil { return nil, &fleet.BadRequestError{Message: "invalid base64 encoding for uninstall_script"} } if decoded.PreInstallQuery, err = decodeBase64Script(decoded.PreInstallQuery); err != nil { return nil, &fleet.BadRequestError{Message: "invalid base64 encoding for pre_install_query"} } if decoded.PostInstallScript, err = decodeBase64Script(decoded.PostInstallScript); err != nil { return nil, &fleet.BadRequestError{Message: "invalid base64 encoding for post_install_script"} } } return &decoded, nil } func (r uploadSoftwareInstallerResponse) Error() error { return r.Err } func uploadSoftwareInstallerEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*uploadSoftwareInstallerRequest) ff, err := req.File.Open() if err != nil { return uploadSoftwareInstallerResponse{Err: err}, nil } defer ff.Close() tfr, err := fleet.NewTempFileReader(ff, nil) if err != nil { return uploadSoftwareInstallerResponse{Err: err}, nil } defer tfr.Close() payload := &fleet.UploadSoftwareInstallerPayload{ TeamID: req.TeamID, InstallScript: req.InstallScript, PreInstallQuery: req.PreInstallQuery, PostInstallScript: req.PostInstallScript, InstallerFile: tfr, Filename: req.File.Filename, SelfService: req.SelfService, UninstallScript: req.UninstallScript, LabelsIncludeAny: req.LabelsIncludeAny, LabelsExcludeAny: req.LabelsExcludeAny, AutomaticInstall: req.AutomaticInstall, } installer, err := svc.UploadSoftwareInstaller(ctx, payload) if err != nil { return uploadSoftwareInstallerResponse{Err: err}, nil } return &uploadSoftwareInstallerResponse{SoftwarePackage: installer}, nil } func (svc *Service) UploadSoftwareInstaller(ctx context.Context, payload *fleet.UploadSoftwareInstallerPayload) (*fleet.SoftwareInstaller, error) { // skipauth: No authorization check needed due to implementation returning // only license error. svc.authz.SkipAuthorization(ctx) return nil, fleet.ErrMissingLicense } type deleteSoftwareInstallerRequest struct { TeamID *uint `query:"team_id"` TitleID uint `url:"title_id"` } type deleteSoftwareInstallerResponse struct { Err error `json:"error,omitempty"` } func (r deleteSoftwareInstallerResponse) Error() error { return r.Err } func (r deleteSoftwareInstallerResponse) Status() int { return http.StatusNoContent } func deleteSoftwareInstallerEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*deleteSoftwareInstallerRequest) err := svc.DeleteSoftwareInstaller(ctx, req.TitleID, req.TeamID) if err != nil { return deleteSoftwareInstallerResponse{Err: err}, nil } return deleteSoftwareInstallerResponse{}, nil } func (svc *Service) DeleteSoftwareInstaller(ctx context.Context, titleID uint, teamID *uint) error { // skipauth: No authorization check needed due to implementation returning // only license error. svc.authz.SkipAuthorization(ctx) return fleet.ErrMissingLicense } type getSoftwareInstallerRequest struct { Alt string `query:"alt,optional"` TeamID *uint `query:"team_id"` TitleID uint `url:"title_id"` } type downloadSoftwareInstallerRequest struct { TitleID uint `url:"title_id"` Token string `url:"token"` } func getSoftwareInstallerEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getSoftwareInstallerRequest) payload, err := svc.DownloadSoftwareInstaller(ctx, false, req.Alt, req.TitleID, req.TeamID) if err != nil { return orbitDownloadSoftwareInstallerResponse{Err: err}, nil } return orbitDownloadSoftwareInstallerResponse{payload: payload}, nil } func getSoftwareInstallerTokenEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getSoftwareInstallerRequest) token, err := svc.GenerateSoftwareInstallerToken(ctx, req.Alt, req.TitleID, req.TeamID) if err != nil { return getSoftwareInstallerTokenResponse{Err: err}, nil } return getSoftwareInstallerTokenResponse{Token: token}, nil } func downloadSoftwareInstallerEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*downloadSoftwareInstallerRequest) meta, err := svc.GetSoftwareInstallerTokenMetadata(ctx, req.Token, req.TitleID) if err != nil { return orbitDownloadSoftwareInstallerResponse{Err: err}, nil } payload, err := svc.DownloadSoftwareInstaller(ctx, true, "media", meta.TitleID, &meta.TeamID) if err != nil { return orbitDownloadSoftwareInstallerResponse{Err: err}, nil } return orbitDownloadSoftwareInstallerResponse{payload: payload}, nil } func (svc *Service) GenerateSoftwareInstallerToken(ctx context.Context, _ string, _ uint, _ *uint) (string, error) { // skipauth: No authorization check needed due to implementation returning // only license error. svc.authz.SkipAuthorization(ctx) return "", fleet.ErrMissingLicense } func (svc *Service) GetSoftwareInstallerTokenMetadata(ctx context.Context, _ string, _ uint) (*fleet.SoftwareInstallerTokenMetadata, error, ) { // skipauth: No authorization check needed due to implementation returning // only license error. svc.authz.SkipAuthorization(ctx) return nil, fleet.ErrMissingLicense } func (svc *Service) GetSoftwareInstallerMetadata(ctx context.Context, _ bool, _ uint, _ *uint) (*fleet.SoftwareInstaller, error) { // skipauth: No authorization check needed due to implementation returning // only license error. svc.authz.SkipAuthorization(ctx) return nil, fleet.ErrMissingLicense } type getSoftwareInstallerResponse struct { SoftwareInstaller *fleet.SoftwareInstaller `json:"software_installer,omitempty"` Err error `json:"error,omitempty"` } func (r getSoftwareInstallerResponse) Error() error { return r.Err } type getSoftwareInstallerTokenResponse struct { Err error `json:"error,omitempty"` Token string `json:"token"` } func (r getSoftwareInstallerTokenResponse) Error() error { return r.Err } type orbitDownloadSoftwareInstallerResponse struct { Err error `json:"error,omitempty"` // fields used by hijackRender for the response. payload *fleet.DownloadSoftwareInstallerPayload } func (r orbitDownloadSoftwareInstallerResponse) Error() error { return r.Err } func (r orbitDownloadSoftwareInstallerResponse) HijackRender(ctx context.Context, w http.ResponseWriter) { w.Header().Set("Content-Length", strconv.Itoa(int(r.payload.Size))) w.Header().Set("Content-Type", "application/octet-stream") w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment;filename="%s"`, r.payload.Filename)) // OK to just log the error here as writing anything on // `http.ResponseWriter` sets the status code to 200 (and it can't be // changed.) Clients should rely on matching content-length with the // header provided if n, err := io.Copy(w, r.payload.Installer); err != nil { logging.WithExtras(ctx, "err", err, "bytes_copied", n) } r.payload.Installer.Close() } func (svc *Service) DownloadSoftwareInstaller(ctx context.Context, _ bool, _ string, _ uint, _ *uint) (*fleet.DownloadSoftwareInstallerPayload, error, ) { // skipauth: No authorization check needed due to implementation returning // only license error. svc.authz.SkipAuthorization(ctx) return nil, fleet.ErrMissingLicense } ///////////////////////////////////////////////////////////////////////////////// // Request to install software in a host ///////////////////////////////////////////////////////////////////////////////// type installSoftwareRequest struct { HostID uint `url:"host_id"` SoftwareTitleID uint `url:"software_title_id"` } type installSoftwareResponse struct { Err error `json:"error,omitempty"` } func (r installSoftwareResponse) Error() error { return r.Err } func (r installSoftwareResponse) Status() int { return http.StatusAccepted } func installSoftwareTitleEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*installSoftwareRequest) err := svc.InstallSoftwareTitle(ctx, req.HostID, req.SoftwareTitleID) if err != nil { return installSoftwareResponse{Err: err}, nil } return installSoftwareResponse{}, nil } func (svc *Service) InstallSoftwareTitle(ctx context.Context, hostID uint, softwareTitleID uint) error { // skipauth: No authorization check needed due to implementation returning // only license error. svc.authz.SkipAuthorization(ctx) return fleet.ErrMissingLicense } func (svc *Service) GetVPPTokenIfCanInstallVPPApps(ctx context.Context, appleDevice bool, host *fleet.Host) (string, error) { return "", fleet.ErrMissingLicense // called downstream of auth checks so doesn't need skipauth } func (svc *Service) InstallVPPAppPostValidation(ctx context.Context, host *fleet.Host, vppApp *fleet.VPPApp, token string, opts fleet.HostSoftwareInstallOptions) (string, error) { return "", fleet.ErrMissingLicense // called downstream of auth checks so doesn't need skipauth } //////////////////////////////////////////////////////////////////////////////// // Uninstall software //////////////////////////////////////////////////////////////////////////////// type uninstallSoftwareRequest struct { HostID uint `url:"host_id"` SoftwareTitleID uint `url:"software_title_id"` } func uninstallSoftwareTitleEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*uninstallSoftwareRequest) err := svc.UninstallSoftwareTitle(ctx, req.HostID, req.SoftwareTitleID) if err != nil { return installSoftwareResponse{Err: err}, nil } return installSoftwareResponse{}, nil } func (svc *Service) UninstallSoftwareTitle(ctx context.Context, _ uint, _ uint) error { // skipauth: No authorization check needed due to implementation returning only license error. svc.authz.SkipAuthorization(ctx) return fleet.ErrMissingLicense } //////////////////////////////////////////////////////////////////////////////// // Get software uninstall results (host details and self service) //////////////////////////////////////////////////////////////////////////////// type getSoftwareInstallResultsRequest struct { InstallUUID string `url:"install_uuid"` } type getDeviceSoftwareInstallResultsRequest struct { Token string `url:"token"` InstallUUID string `url:"install_uuid"` } func (r *getDeviceSoftwareInstallResultsRequest) deviceAuthToken() string { return r.Token } type getSoftwareInstallResultsResponse struct { Err error `json:"error,omitempty"` Results *fleet.HostSoftwareInstallerResult `json:"results,omitempty"` } func (r getSoftwareInstallResultsResponse) Error() error { return r.Err } func getDeviceSoftwareInstallResultsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { _, ok := hostctx.FromContext(ctx) if !ok { err := ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("internal error: missing host from request context")) return getSoftwareInstallResultsResponse{Err: err}, nil } req := request.(*getDeviceSoftwareInstallResultsRequest) results, err := svc.GetSoftwareInstallResults(ctx, req.InstallUUID) if err != nil { return getSoftwareInstallResultsResponse{Err: err}, nil } return &getSoftwareInstallResultsResponse{Results: results}, nil } func getSoftwareInstallResultsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getSoftwareInstallResultsRequest) results, err := svc.GetSoftwareInstallResults(ctx, req.InstallUUID) if err != nil { return getSoftwareInstallResultsResponse{Err: err}, nil } return &getSoftwareInstallResultsResponse{Results: results}, nil } func (svc *Service) GetSoftwareInstallResults(ctx context.Context, resultUUID string) (*fleet.HostSoftwareInstallerResult, error) { // skipauth: No authorization check needed due to implementation returning // only license error. svc.authz.SkipAuthorization(ctx) return nil, fleet.ErrMissingLicense } //////////////////////////////////////////////////////////////////////////////// // Get software uninstall results from My device page //////////////////////////////////////////////////////////////////////////////// type getDeviceSoftwareUninstallResultsRequest struct { Token string `url:"token"` ExecutionID string `url:"execution_id"` } func (r *getDeviceSoftwareUninstallResultsRequest) deviceAuthToken() string { return r.Token } func getDeviceSoftwareUninstallResultsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { host, ok := hostctx.FromContext(ctx) if !ok { err := ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("internal error: missing host from request context")) return getSoftwareInstallResultsResponse{Err: err}, nil } req := request.(*getDeviceSoftwareUninstallResultsRequest) scriptResult, err := svc.GetSelfServiceUninstallScriptResult(ctx, host, req.ExecutionID) if err != nil { return getScriptResultResponse{Err: err}, nil } return setUpGetScriptResultResponse(scriptResult), nil } func (svc *Service) GetSelfServiceUninstallScriptResult(ctx context.Context, host *fleet.Host, execID string) (*fleet.HostScriptResult, error) { // skipauth: No authorization check needed due to implementation returning // only license error. svc.authz.SkipAuthorization(ctx) return nil, fleet.ErrMissingLicense } //////////////////////////////////////////////////////////////////////////////// // Batch replace software installers //////////////////////////////////////////////////////////////////////////////// type batchSetSoftwareInstallersRequest struct { TeamName string `json:"-" query:"team_name,optional"` DryRun bool `json:"-" query:"dry_run,optional"` // if true, apply validation but do not save changes Software []*fleet.SoftwareInstallerPayload `json:"software"` } type batchSetSoftwareInstallersResponse struct { RequestUUID string `json:"request_uuid"` Err error `json:"error,omitempty"` } func (r batchSetSoftwareInstallersResponse) Error() error { return r.Err } func (r batchSetSoftwareInstallersResponse) Status() int { return http.StatusAccepted } func batchSetSoftwareInstallersEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*batchSetSoftwareInstallersRequest) requestUUID, err := svc.BatchSetSoftwareInstallers(ctx, req.TeamName, req.Software, req.DryRun) if err != nil { return batchSetSoftwareInstallersResponse{Err: err}, nil } return batchSetSoftwareInstallersResponse{RequestUUID: requestUUID}, nil } func (svc *Service) BatchSetSoftwareInstallers(ctx context.Context, tmName string, payloads []*fleet.SoftwareInstallerPayload, dryRun bool) (string, error) { // skipauth: No authorization check needed due to implementation returning // only license error. svc.authz.SkipAuthorization(ctx) return "", fleet.ErrMissingLicense } type batchSetSoftwareInstallersResultRequest struct { RequestUUID string `url:"request_uuid"` TeamName string `query:"team_name,optional"` DryRun bool `query:"dry_run,optional"` // if true, apply validation but do not save changes } type batchSetSoftwareInstallersResultResponse struct { Status string `json:"status"` Message string `json:"message"` Packages []fleet.SoftwarePackageResponse `json:"packages"` Err error `json:"error,omitempty"` } func (r batchSetSoftwareInstallersResultResponse) Error() error { return r.Err } func batchSetSoftwareInstallersResultEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*batchSetSoftwareInstallersResultRequest) status, message, packages, err := svc.GetBatchSetSoftwareInstallersResult(ctx, req.TeamName, req.RequestUUID, req.DryRun) if err != nil { return batchSetSoftwareInstallersResultResponse{Err: err}, nil } return batchSetSoftwareInstallersResultResponse{ Status: status, Message: message, Packages: packages, }, nil } func (svc *Service) GetBatchSetSoftwareInstallersResult(ctx context.Context, tmName string, requestUUID string, dryRun bool) (string, string, []fleet.SoftwarePackageResponse, error) { // skipauth: No authorization check needed due to implementation returning // only license error. svc.authz.SkipAuthorization(ctx) return "", "", nil, fleet.ErrMissingLicense } ////////////////////////////////////////////////////////////////////////////// // Self Service Install ////////////////////////////////////////////////////////////////////////////// type fleetSelfServiceSoftwareInstallRequest struct { Token string `url:"token"` SoftwareTitleID uint `url:"software_title_id"` } func (r *fleetSelfServiceSoftwareInstallRequest) deviceAuthToken() string { return r.Token } type submitSelfServiceSoftwareInstallResponse struct { Err error `json:"error,omitempty"` } func (r submitSelfServiceSoftwareInstallResponse) Error() error { return r.Err } func (r submitSelfServiceSoftwareInstallResponse) Status() int { return http.StatusAccepted } func submitSelfServiceSoftwareInstall(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { host, ok := hostctx.FromContext(ctx) if !ok { err := ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("internal error: missing host from request context")) return submitSelfServiceSoftwareInstallResponse{Err: err}, nil } req := request.(*fleetSelfServiceSoftwareInstallRequest) if err := svc.SelfServiceInstallSoftwareTitle(ctx, host, req.SoftwareTitleID); err != nil { return submitSelfServiceSoftwareInstallResponse{Err: err}, nil } return submitSelfServiceSoftwareInstallResponse{}, nil } func (svc *Service) SelfServiceInstallSoftwareTitle(ctx context.Context, host *fleet.Host, softwareTitleID uint) error { // skipauth: No authorization check needed due to implementation returning // only license error. svc.authz.SkipAuthorization(ctx) return fleet.ErrMissingLicense } type fleetDeviceSoftwareUninstallRequest struct { Token string `url:"token"` SoftwareTitleID uint `url:"software_title_id"` } func (r *fleetDeviceSoftwareUninstallRequest) deviceAuthToken() string { return r.Token } type submitDeviceSoftwareUninstallResponse struct { Err error `json:"error,omitempty"` } func (r submitDeviceSoftwareUninstallResponse) Error() error { return r.Err } func (r submitDeviceSoftwareUninstallResponse) Status() int { return http.StatusAccepted } func submitDeviceSoftwareUninstall(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { host, ok := hostctx.FromContext(ctx) if !ok { err := ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("internal error: missing host from request context")) return submitDeviceSoftwareUninstallResponse{Err: err}, nil } req := request.(*fleetDeviceSoftwareUninstallRequest) if err := svc.UninstallSoftwareTitle(ctx, host.ID, req.SoftwareTitleID); err != nil { return submitDeviceSoftwareUninstallResponse{Err: err}, nil } return submitDeviceSoftwareUninstallResponse{}, nil } func (svc *Service) HasSelfServiceSoftwareInstallers(ctx context.Context, host *fleet.Host) (bool, error) { alreadyAuthenticated := svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceToken) || svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceCertificate) || svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceURL) if !alreadyAuthenticated { if err := svc.authz.Authorize(ctx, host, fleet.ActionRead); err != nil { return false, err } } return svc.ds.HasSelfServiceSoftwareInstallers(ctx, host.Platform, host.TeamID) } ////////////////////////////////////////////////////////////////////////////// // VPP App Store Apps Batch Install ////////////////////////////////////////////////////////////////////////////// type batchAssociateAppStoreAppsRequest struct { TeamName string `json:"-" query:"team_name,optional"` DryRun bool `json:"-" query:"dry_run,optional"` Apps []fleet.VPPBatchPayload `json:"app_store_apps"` } func (b *batchAssociateAppStoreAppsRequest) DecodeBody(ctx context.Context, r io.Reader, u url.Values, c []*x509.Certificate) error { if err := json.NewDecoder(r).Decode(b); err != nil { var typeErr *json.UnmarshalTypeError if errors.As(err, &typeErr) { return ctxerr.Wrap(ctx, fleet.NewUserMessageError(fmt.Errorf("Couldn't edit software. %q must be a %s, found %s", typeErr.Field, typeErr.Type.String(), typeErr.Value), http.StatusBadRequest)) } } return nil } type batchAssociateAppStoreAppsResponse struct { Apps []fleet.VPPAppResponse `json:"app_store_apps"` Err error `json:"error,omitempty"` } func (r batchAssociateAppStoreAppsResponse) Error() error { return r.Err } func batchAssociateAppStoreAppsEndpoint(ctx context.Context, request any, svc fleet.Service) (fleet.Errorer, error) { req := request.(*batchAssociateAppStoreAppsRequest) apps, err := svc.BatchAssociateVPPApps(ctx, req.TeamName, req.Apps, req.DryRun) if err != nil { return batchAssociateAppStoreAppsResponse{Err: err}, nil } return batchAssociateAppStoreAppsResponse{Apps: apps}, nil } func (svc *Service) BatchAssociateVPPApps(ctx context.Context, teamName string, payloads []fleet.VPPBatchPayload, dryRun bool) ([]fleet.VPPAppResponse, error) { // skipauth: No authorization check needed due to implementation returning // only license error. svc.authz.SkipAuthorization(ctx) return nil, fleet.ErrMissingLicense } type getInHouseAppManifestRequest struct { TitleID uint `url:"title_id"` TeamID *uint `query:"team_id"` } type getInHouseAppManifestResponse struct { // Manifest field is used in HijackRender for the response. Manifest []byte Err error `json:"error,omitempty"` } func (r getInHouseAppManifestResponse) Error() error { return r.Err } func (r getInHouseAppManifestResponse) HijackRender(ctx context.Context, w http.ResponseWriter) { // make the browser download the content to a file w.Header().Add("Content-Disposition", `attachment; filename="in-house-app-manifest.plist"`) // explicitly set the content length before the write, so the caller can // detect short writes (if it fails to send the full content properly) w.Header().Set("Content-Length", strconv.FormatInt(int64(len(r.Manifest)), 10)) // this content type will make macos open the profile with the proper application w.Header().Set("Content-Type", "application/x-apple-aspen-config; charset=utf-8") // prevent detection of content, obey the provided content-type w.Header().Set("X-Content-Type-Options", "nosniff") if n, err := w.Write(r.Manifest); err != nil { logging.WithExtras(ctx, "err", err, "written", n) } } func getInHouseAppManifestEndpoint(ctx context.Context, request any, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getInHouseAppManifestRequest) manifest, err := svc.GetInHouseAppManifest(ctx, req.TitleID, req.TeamID) if err != nil { return &getInHouseAppManifestResponse{Err: err}, nil } return &getInHouseAppManifestResponse{Manifest: manifest}, nil } func (svc *Service) GetInHouseAppManifest(ctx context.Context, titleID uint, teamID *uint) ([]byte, error) { // skipauth: No authorization check needed due to implementation returning // only license error. svc.authz.SkipAuthorization(ctx) return nil, fleet.ErrMissingLicense } type getInHouseAppPackageRequest struct { TitleID uint `url:"title_id"` TeamID *uint `query:"team_id"` } type getInHouseAppPackageResponse struct { payload *fleet.DownloadSoftwareInstallerPayload Err error `json:"error,omitempty"` } func (r getInHouseAppPackageResponse) HijackRender(ctx context.Context, w http.ResponseWriter) { w.Header().Set("Content-Length", strconv.Itoa(int(r.payload.Size))) w.Header().Set("Content-Type", "application/octet-stream") w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment;filename="%s"`, r.payload.Filename)) // OK to just log the error here as writing anything on // `http.ResponseWriter` sets the status code to 200 (and it can't be // changed.) Clients should rely on matching content-length with the // header provided if n, err := io.Copy(w, r.payload.Installer); err != nil { logging.WithExtras(ctx, "err", err, "bytes_copied", n) } r.payload.Installer.Close() } func (r getInHouseAppPackageResponse) Error() error { return r.Err } func getInHouseAppPackageEndpoint(ctx context.Context, request any, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getInHouseAppPackageRequest) file, err := svc.GetInHouseAppPackage(ctx, req.TitleID, req.TeamID) if err != nil { return &getInHouseAppPackageResponse{Err: err}, nil } return &getInHouseAppPackageResponse{payload: file}, nil } func (svc *Service) GetInHouseAppPackage(ctx context.Context, titleID uint, teamID *uint) (*fleet.DownloadSoftwareInstallerPayload, error) { // skipauth: No authorization check needed due to implementation returning // only license error. svc.authz.SkipAuthorization(ctx) return nil, fleet.ErrMissingLicense }