mirror of
https://github.com/fleetdm/fleet
synced 2026-05-21 07:58:31 +00:00
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** For #33391 ## Testing - [X] Added/updated automated tests there's a number of tests for this, if they still pass we're in good shape - [X] QA'd all new/changed functionality manually I tested the front-end successfully, and saw an auto-update go through on an ipad. Also verified that the activity metadata is correct.
430 lines
14 KiB
Go
430 lines
14 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"mime/multipart"
|
|
"net/http"
|
|
|
|
"github.com/docker/go-units"
|
|
"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"
|
|
)
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
// Get App Store apps
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
|
|
type getAppStoreAppsRequest struct {
|
|
TeamID uint `query:"team_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"`
|
|
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"`
|
|
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,
|
|
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"`
|
|
SelfService *bool `json:"self_service"`
|
|
LabelsIncludeAny []string `json:"labels_include_any"`
|
|
LabelsExcludeAny []string `json:"labels_exclude_any"`
|
|
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,
|
|
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(512 * units.MiB)
|
|
if err != nil {
|
|
return nil, &fleet.BadRequestError{
|
|
Message: "failed to parse multipart form",
|
|
InternalErr: err,
|
|
}
|
|
}
|
|
|
|
if r.MultipartForm.File["token"] == nil || 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(512 * units.MiB)
|
|
if err != nil {
|
|
return nil, &fleet.BadRequestError{
|
|
Message: "failed to parse multipart form",
|
|
InternalErr: err,
|
|
}
|
|
}
|
|
|
|
if r.MultipartForm.File["token"] == nil || 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"`
|
|
}
|
|
|
|
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
|
|
}
|