package service import ( "context" "encoding/json" "io" "mime/multipart" "net/http" "github.com/fleetdm/fleet/v4/server/authz" "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "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" ) ////////////////////////////////////////////////////////////////////////////// // Get App Store apps ////////////////////////////////////////////////////////////////////////////// type getAppStoreAppsRequest struct { TeamID uint `query:"team_id" renameto:"fleet_id"` } type getAppStoreAppsResponse struct { AppStoreApps []*fleet.VPPApp `json:"app_store_apps"` Err error `json:"error,omitempty"` } func (r getAppStoreAppsResponse) Error() error { return r.Err } func getAppStoreAppsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getAppStoreAppsRequest) apps, err := svc.GetAppStoreApps(ctx, &req.TeamID) if err != nil { return &getAppStoreAppsResponse{Err: err}, nil } return &getAppStoreAppsResponse{AppStoreApps: apps}, nil } func (svc *Service) GetAppStoreApps(ctx context.Context, teamID *uint) ([]*fleet.VPPApp, error) { // skipauth: No authorization check needed due to implementation returning // only license error. svc.authz.SkipAuthorization(ctx) return nil, fleet.ErrMissingLicense } ////////////////////////////////////////////////////////////////////////////// // Add App Store apps ////////////////////////////////////////////////////////////////////////////// type addAppStoreAppRequest struct { TeamID *uint `json:"team_id" renameto:"fleet_id"` AppStoreID string `json:"app_store_id"` Platform fleet.InstallableDevicePlatform `json:"platform"` SelfService bool `json:"self_service"` AutomaticInstall bool `json:"automatic_install"` LabelsIncludeAny []string `json:"labels_include_any"` LabelsExcludeAny []string `json:"labels_exclude_any"` LabelsIncludeAll []string `json:"labels_include_all"` Categories []string `json:"categories"` Configuration json.RawMessage `json:"configuration,omitempty"` } type addAppStoreAppResponse struct { TitleID uint `json:"software_title_id,omitempty"` Err error `json:"error,omitempty"` } func (r addAppStoreAppResponse) Error() error { return r.Err } func addAppStoreAppEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*addAppStoreAppRequest) titleID, err := svc.AddAppStoreApp(ctx, req.TeamID, fleet.VPPAppTeam{ VPPAppID: fleet.VPPAppID{AdamID: req.AppStoreID, Platform: req.Platform}, SelfService: req.SelfService, LabelsIncludeAny: req.LabelsIncludeAny, LabelsExcludeAny: req.LabelsExcludeAny, LabelsIncludeAll: req.LabelsIncludeAll, AddAutoInstallPolicy: req.AutomaticInstall, Categories: req.Categories, Configuration: req.Configuration, }) if err != nil { return &addAppStoreAppResponse{Err: err}, nil } return &addAppStoreAppResponse{TitleID: titleID}, nil } func (svc *Service) AddAppStoreApp(ctx context.Context, _ *uint, _ fleet.VPPAppTeam) (uint, error) { // skipauth: No authorization check needed due to implementation returning // only license error. svc.authz.SkipAuthorization(ctx) return 0, fleet.ErrMissingLicense } ////////////////////////////////////////////////////////////////////////////// // Update App Store apps ////////////////////////////////////////////////////////////////////////////// type updateAppStoreAppRequest struct { TitleID uint `url:"title_id"` TeamID *uint `json:"team_id" renameto:"fleet_id"` SelfService *bool `json:"self_service"` LabelsIncludeAny []string `json:"labels_include_any"` LabelsExcludeAny []string `json:"labels_exclude_any"` LabelsIncludeAll []string `json:"labels_include_all"` Categories []string `json:"categories"` Configuration json.RawMessage `json:"configuration,omitempty"` DisplayName *string `json:"display_name"` AutoUpdateEnabled *bool `json:"auto_update_enabled,omitempty"` // AutoUpdateStartTime is the beginning of the maintenance window for the software title. // This is only applicable when viewing a title in the context of a team. AutoUpdateStartTime *string `json:"auto_update_window_start,omitempty"` // AutoUpdateStartTime is the end of the maintenance window for the software title. // If the end time is less than the start time, the window wraps to the next day. // This is only applicable when viewing a title in the context of a team. AutoUpdateEndTime *string `json:"auto_update_window_end,omitempty"` } type updateAppStoreAppResponse struct { AppStoreApp *fleet.VPPAppStoreApp `json:"app_store_app,omitempty"` Err error `json:"error,omitempty"` } func (r updateAppStoreAppResponse) Error() error { return r.Err } func updateAppStoreAppEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*updateAppStoreAppRequest) updatedApp, activity, err := svc.UpdateAppStoreApp(ctx, req.TitleID, req.TeamID, fleet.AppStoreAppUpdatePayload{ SelfService: req.SelfService, LabelsIncludeAny: req.LabelsIncludeAny, LabelsExcludeAny: req.LabelsExcludeAny, LabelsIncludeAll: req.LabelsIncludeAll, Categories: req.Categories, Configuration: req.Configuration, DisplayName: req.DisplayName, SoftwareAutoUpdateConfig: fleet.SoftwareAutoUpdateConfig{ AutoUpdateEnabled: req.AutoUpdateEnabled, AutoUpdateStartTime: req.AutoUpdateStartTime, AutoUpdateEndTime: req.AutoUpdateEndTime, }, }) if err != nil { return updateAppStoreAppResponse{Err: err}, nil } if req.AutoUpdateEnabled != nil { // Update AutoUpdateConfig separately err = svc.UpdateSoftwareTitleAutoUpdateConfig(ctx, req.TitleID, req.TeamID, fleet.SoftwareAutoUpdateConfig{ AutoUpdateEnabled: req.AutoUpdateEnabled, AutoUpdateStartTime: req.AutoUpdateStartTime, AutoUpdateEndTime: req.AutoUpdateEndTime, }) if err != nil { return updateAppStoreAppResponse{Err: err}, nil } } // Re-fetch the software title to get the updated auto-update config. updatedTitle, err := svc.SoftwareTitleByID(ctx, req.TitleID, req.TeamID) if err != nil { return updateAppStoreAppResponse{Err: err}, nil } if updatedTitle.AutoUpdateEnabled != nil { activity.AutoUpdateEnabled = updatedTitle.AutoUpdateEnabled if *updatedTitle.AutoUpdateEnabled { activity.AutoUpdateStartTime = updatedTitle.AutoUpdateStartTime activity.AutoUpdateEndTime = updatedTitle.AutoUpdateEndTime } } if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), activity); err != nil { return updateAppStoreAppResponse{Err: err}, nil } return updateAppStoreAppResponse{AppStoreApp: updatedApp}, nil } func (svc *Service) UpdateAppStoreApp(ctx context.Context, titleID uint, teamID *uint, payload fleet.AppStoreAppUpdatePayload) (*fleet.VPPAppStoreApp, *fleet.ActivityEditedAppStoreApp, error) { // skipauth: No authorization check needed due to implementation returning // only license error. svc.authz.SkipAuthorization(ctx) return nil, nil, fleet.ErrMissingLicense } //////////////////////////////////////////////////////////////////////////////// // POST /api/_version_/vpp_tokens //////////////////////////////////////////////////////////////////////////////// type uploadVPPTokenRequest struct { File *multipart.FileHeader } func (uploadVPPTokenRequest) DecodeRequest(ctx context.Context, r *http.Request) (interface{}, error) { decoded := uploadVPPTokenRequest{} err := r.ParseMultipartForm(platform_http.MaxMultipartFormSize) if err != nil { return nil, &fleet.BadRequestError{ Message: "failed to parse multipart form", InternalErr: err, } } if len(r.MultipartForm.File["token"]) == 0 { return nil, &fleet.BadRequestError{ Message: "token multipart field is required", InternalErr: err, } } decoded.File = r.MultipartForm.File["token"][0] return &decoded, nil } type uploadVPPTokenResponse struct { Err error `json:"error,omitempty"` Token *fleet.VPPTokenDB `json:"token,omitempty"` } func (r uploadVPPTokenResponse) Status() int { return http.StatusAccepted } func (r uploadVPPTokenResponse) Error() error { return r.Err } func uploadVPPTokenEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*uploadVPPTokenRequest) file, err := req.File.Open() if err != nil { return uploadVPPTokenResponse{Err: err}, nil } defer file.Close() tok, err := svc.UploadVPPToken(ctx, file) if err != nil { return uploadVPPTokenResponse{Err: err}, nil } return uploadVPPTokenResponse{Token: tok}, nil } func (svc *Service) UploadVPPToken(ctx context.Context, file io.ReadSeeker) (*fleet.VPPTokenDB, error) { // skipauth: No authorization check needed due to implementation returning // only license error. svc.authz.SkipAuthorization(ctx) return nil, fleet.ErrMissingLicense } //////////////////////////////////////////////////// // PATCH /api/_version_/fleet/vpp_tokens/%d/renew // //////////////////////////////////////////////////// type patchVPPTokenRenewRequest struct { ID uint `url:"id"` File *multipart.FileHeader } func (patchVPPTokenRenewRequest) DecodeRequest(ctx context.Context, r *http.Request) (interface{}, error) { decoded := patchVPPTokenRenewRequest{} err := r.ParseMultipartForm(platform_http.MaxMultipartFormSize) if err != nil { return nil, &fleet.BadRequestError{ Message: "failed to parse multipart form", InternalErr: err, } } if len(r.MultipartForm.File["token"]) == 0 { return nil, &fleet.BadRequestError{ Message: "token multipart field is required", InternalErr: err, } } decoded.File = r.MultipartForm.File["token"][0] id, err := endpointer.UintFromRequest(r, "id") if err != nil { return nil, ctxerr.Wrap(ctx, err, "failed to parse vpp token id") } decoded.ID = uint(id) //nolint:gosec // dismiss G115 return &decoded, nil } type patchVPPTokenRenewResponse struct { Err error `json:"error,omitempty"` Token *fleet.VPPTokenDB `json:"token,omitempty"` } func (r patchVPPTokenRenewResponse) Status() int { return http.StatusAccepted } func (r patchVPPTokenRenewResponse) Error() error { return r.Err } func patchVPPTokenRenewEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*patchVPPTokenRenewRequest) file, err := req.File.Open() if err != nil { return patchVPPTokenRenewResponse{Err: err}, nil } defer file.Close() tok, err := svc.UpdateVPPToken(ctx, req.ID, file) if err != nil { return patchVPPTokenRenewResponse{Err: err}, nil } return patchVPPTokenRenewResponse{Token: tok}, nil } func (svc *Service) UpdateVPPToken(ctx context.Context, tokenID uint, token io.ReadSeeker) (*fleet.VPPTokenDB, error) { // skipauth: No authorization check needed due to implementation returning // only license error. svc.authz.SkipAuthorization(ctx) return nil, fleet.ErrMissingLicense } //////////////////////////////////////////////////// // PATCH /api/_version_/fleet/vpp_tokens/%d/teams // //////////////////////////////////////////////////// type patchVPPTokensTeamsRequest struct { ID uint `url:"id"` TeamIDs []uint `json:"teams" renameto:"fleets"` } type patchVPPTokensTeamsResponse struct { Token *fleet.VPPTokenDB `json:"token,omitempty"` Err error `json:"error,omitempty"` } func (r patchVPPTokensTeamsResponse) Error() error { return r.Err } func patchVPPTokensTeams(ctx context.Context, request any, svc fleet.Service) (fleet.Errorer, error) { req := request.(*patchVPPTokensTeamsRequest) tok, err := svc.UpdateVPPTokenTeams(ctx, req.ID, req.TeamIDs) if err != nil { return patchVPPTokensTeamsResponse{Err: err}, nil } return patchVPPTokensTeamsResponse{Token: tok}, nil } func (svc *Service) UpdateVPPTokenTeams(ctx context.Context, tokenID uint, teamIDs []uint) (*fleet.VPPTokenDB, error) { // skipauth: No authorization check needed due to implementation returning // only license error. svc.authz.SkipAuthorization(ctx) return nil, fleet.ErrMissingLicense } ///////////////////////////////////////// // GET /api/_version_/fleet/vpp_tokens // ///////////////////////////////////////// type getVPPTokensRequest struct{} type getVPPTokensResponse struct { Tokens []*fleet.VPPTokenDB `json:"vpp_tokens"` Err error `json:"error,omitempty"` } func (r getVPPTokensResponse) Error() error { return r.Err } func getVPPTokens(ctx context.Context, request any, svc fleet.Service) (fleet.Errorer, error) { tokens, err := svc.GetVPPTokens(ctx) if err != nil { return getVPPTokensResponse{Err: err}, nil } if tokens == nil { tokens = []*fleet.VPPTokenDB{} } return getVPPTokensResponse{Tokens: tokens}, nil } func (svc *Service) GetVPPTokens(ctx context.Context) ([]*fleet.VPPTokenDB, error) { // skipauth: No authorization check needed due to implementation returning // only license error. svc.authz.SkipAuthorization(ctx) return nil, fleet.ErrMissingLicense } /////////////////////////////////////////////// // DELETE /api/_version_/fleet/vpp_tokens/%d // /////////////////////////////////////////////// type deleteVPPTokenRequest struct { ID uint `url:"id"` } type deleteVPPTokenResponse struct { Err error `json:"error,omitempty"` } func (r deleteVPPTokenResponse) Error() error { return r.Err } func (r deleteVPPTokenResponse) Status() int { return http.StatusNoContent } func deleteVPPToken(ctx context.Context, request any, svc fleet.Service) (fleet.Errorer, error) { req := request.(*deleteVPPTokenRequest) err := svc.DeleteVPPToken(ctx, req.ID) if err != nil { return deleteVPPTokenResponse{Err: err}, nil } return deleteVPPTokenResponse{}, nil } func (svc *Service) DeleteVPPToken(ctx context.Context, tokenID uint) error { // skipauth: No authorization check needed due to implementation returning // only license error. svc.authz.SkipAuthorization(ctx) return fleet.ErrMissingLicense } //////////////////////////////////////////////////////////////////////////////// // POST /api/_version_/software/web_apps //////////////////////////////////////////////////////////////////////////////// type createAndroidWebAppRequest struct { Title string URL string Icon *multipart.FileHeader } func (createAndroidWebAppRequest) DecodeRequest(ctx context.Context, r *http.Request) (any, error) { decoded := createAndroidWebAppRequest{} err := r.ParseMultipartForm(platform_http.MaxMultipartFormSize) if err != nil { return nil, &fleet.BadRequestError{ Message: "failed to parse multipart form", InternalErr: err, } } title := r.FormValue("title") if title == "" { return nil, &fleet.BadRequestError{Message: "title multipart field is required"} } decoded.Title = title url := r.FormValue("url") if url == "" { return nil, &fleet.BadRequestError{Message: "url multipart field is required"} } decoded.URL = url if len(r.MultipartForm.File["icon"]) > 0 { decoded.Icon = r.MultipartForm.File["icon"][0] } return &decoded, nil } type createAndroidWebAppResponse struct { AppStoreID string `json:"app_store_id"` Err error `json:"error,omitempty"` } func (r createAndroidWebAppResponse) Error() error { return r.Err } func createAndroidWebAppEndpoint(ctx context.Context, request any, svc fleet.Service) (fleet.Errorer, error) { req := request.(*createAndroidWebAppRequest) var iconReader io.Reader if req.Icon != nil { f, err := req.Icon.Open() if err != nil { return createAndroidWebAppResponse{Err: err}, nil } defer f.Close() iconReader = f } appID, err := svc.CreateAndroidWebApp(ctx, req.Title, req.URL, iconReader) if err != nil { return createAndroidWebAppResponse{Err: err}, nil } return createAndroidWebAppResponse{AppStoreID: appID}, nil } func (svc *Service) CreateAndroidWebApp(ctx context.Context, title, startURL string, icon io.Reader) (string, error) { // skipauth: No authorization check needed due to implementation returning // only license error. svc.authz.SkipAuthorization(ctx) return "", fleet.ErrMissingLicense }