From a6dcdca2ddffb25e6a77acefe5f94d43b3270403 Mon Sep 17 00:00:00 2001 From: Dante Catalfamo <43040593+dantecatalfamo@users.noreply.github.com> Date: Tue, 3 Sep 2024 09:39:29 -0400 Subject: [PATCH] Validate Premium license when uploading VPP tokens (#21720) #21315 Ensures VPP uploads are behind premium license. Also moved the VPP service methods to the correct file --- changes/21315-vpp-premium-license | 1 + ee/server/service/vpp.go | 144 +++++++++++++ server/service/mdm.go | 333 ------------------------------ server/service/vpp.go | 240 +++++++++++++++++++++ 4 files changed, 385 insertions(+), 333 deletions(-) create mode 100644 changes/21315-vpp-premium-license diff --git a/changes/21315-vpp-premium-license b/changes/21315-vpp-premium-license new file mode 100644 index 0000000000..2fd081703e --- /dev/null +++ b/changes/21315-vpp-premium-license @@ -0,0 +1 @@ +- Verify user has premium license before uploading VPP tokens diff --git a/ee/server/service/vpp.go b/ee/server/service/vpp.go index 52f4c1f890..85c29652ca 100644 --- a/ee/server/service/vpp.go +++ b/ee/server/service/vpp.go @@ -5,6 +5,7 @@ import ( "database/sql" "errors" "fmt" + "io" "net/http" "sort" "strings" @@ -17,6 +18,9 @@ import ( "github.com/fleetdm/fleet/v4/server/mdm/apple/vpp" ) +// Used for overriding the env var value in testing +var testSetEmptyPrivateKey bool + // getVPPToken returns the base64 encoded VPP token, ready for use in requests to Apple's VPP API. // It returns an error if the token is expired. func (svc *Service) getVPPToken(ctx context.Context, teamID *uint) (string, error) { @@ -413,3 +417,143 @@ func getVPPAppsMetadata(ctx context.Context, ids []fleet.VPPAppTeam) ([]*fleet.V return apps, nil } + +func (svc *Service) UploadVPPToken(ctx context.Context, token io.ReadSeeker) (*fleet.VPPTokenDB, error) { + if err := svc.authz.Authorize(ctx, &fleet.AppleCSR{}, fleet.ActionWrite); err != nil { + return nil, err + } + + privateKey := svc.config.Server.PrivateKey + if testSetEmptyPrivateKey { + privateKey = "" + } + + if len(privateKey) == 0 { + return nil, ctxerr.New(ctx, "Couldn't upload content token. Missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key") + } + + if token == nil { + return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("token", "Invalid token. Please provide a valid content token from Apple Business Manager.")) + } + + tokenBytes, err := io.ReadAll(token) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "reading VPP token") + } + + locName, err := vpp.GetConfig(string(tokenBytes)) + if err != nil { + var vppErr *vpp.ErrorResponse + if errors.As(err, &vppErr) { + // Per https://developer.apple.com/documentation/devicemanagement/app_and_book_management/app_and_book_management_legacy/interpreting_error_codes + if vppErr.ErrorNumber == 9622 { + return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("token", "Invalid token. Please provide a valid content token from Apple Business Manager.")) + } + } + return nil, ctxerr.Wrap(ctx, err, "validating VPP token with Apple") + } + + data := fleet.VPPTokenData{ + Token: string(tokenBytes), + Location: locName, + } + + tok, err := svc.ds.InsertVPPToken(ctx, &data) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "writing VPP token to db") + } + + if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityEnabledVPP{ + Location: locName, + }); err != nil { + return nil, ctxerr.Wrap(ctx, err, "create activity for upload VPP token") + } + + return tok, nil +} + +func (svc *Service) UpdateVPPToken(ctx context.Context, tokenID uint, token io.ReadSeeker) (*fleet.VPPTokenDB, error) { + if err := svc.authz.Authorize(ctx, &fleet.AppleCSR{}, fleet.ActionWrite); err != nil { + return nil, err + } + + privateKey := svc.config.Server.PrivateKey + if testSetEmptyPrivateKey { + privateKey = "" + } + + if len(privateKey) == 0 { + return nil, ctxerr.New(ctx, "Couldn't upload content token. Missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key") + } + + if token == nil { + return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("token", "Invalid token. Please provide a valid content token from Apple Business Manager.")) + } + + tokenBytes, err := io.ReadAll(token) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "reading VPP token") + } + + locName, err := vpp.GetConfig(string(tokenBytes)) + if err != nil { + var vppErr *vpp.ErrorResponse + if errors.As(err, &vppErr) { + // Per https://developer.apple.com/documentation/devicemanagement/app_and_book_management/app_and_book_management_legacy/interpreting_error_codes + if vppErr.ErrorNumber == 9622 { + return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("token", "Invalid token. Please provide a valid content token from Apple Business Manager.")) + } + } + return nil, ctxerr.Wrap(ctx, err, "validating VPP token with Apple") + } + + data := fleet.VPPTokenData{ + Token: string(tokenBytes), + Location: locName, + } + + tok, err := svc.ds.UpdateVPPToken(ctx, tokenID, &data) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "updating vpp token") + } + + return tok, nil +} + +func (svc *Service) UpdateVPPTokenTeams(ctx context.Context, tokenID uint, teamIDs []uint) (*fleet.VPPTokenDB, error) { + if err := svc.authz.Authorize(ctx, &fleet.AppleCSR{}, fleet.ActionWrite); err != nil { + return nil, err + } + + tok, err := svc.ds.UpdateVPPTokenTeams(ctx, tokenID, teamIDs) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "updating vpp token team") + } + + return tok, nil +} + +func (svc *Service) GetVPPTokens(ctx context.Context) ([]*fleet.VPPTokenDB, error) { + if err := svc.authz.Authorize(ctx, &fleet.AppleCSR{}, fleet.ActionRead); err != nil { + return nil, err + } + + return svc.ds.ListVPPTokens(ctx) +} + +func (svc *Service) DeleteVPPToken(ctx context.Context, tokenID uint) error { + if err := svc.authz.Authorize(ctx, &fleet.AppleCSR{}, fleet.ActionWrite); err != nil { + return err + } + tok, err := svc.ds.GetVPPToken(ctx, tokenID) + if err != nil { + return ctxerr.Wrap(ctx, err, "getting vpp token") + } + if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityDisabledVPP{ + Location: tok.Location, + }); err != nil { + return ctxerr.Wrap(ctx, err, "create activity for delete VPP token") + } + + return svc.ds.DeleteVPPToken(ctx, tokenID) +} diff --git a/server/service/mdm.go b/server/service/mdm.go index db6ce00944..e50d283ee6 100644 --- a/server/service/mdm.go +++ b/server/service/mdm.go @@ -30,7 +30,6 @@ import ( "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/mdm" apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple" - "github.com/fleetdm/fleet/v4/server/mdm/apple/vpp" "github.com/fleetdm/fleet/v4/server/mdm/assets" nanomdm "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm" "github.com/fleetdm/fleet/v4/server/ptr" @@ -2542,335 +2541,3 @@ func (svc *Service) DeleteMDMAppleAPNSCert(ctx context.Context) error { return svc.ds.SaveAppConfig(ctx, appCfg) } - -//////////////////////////////////////////////////////////////////////////////// -// 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) (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, token io.ReadSeeker) (*fleet.VPPTokenDB, error) { - if err := svc.authz.Authorize(ctx, &fleet.AppleCSR{}, fleet.ActionWrite); err != nil { - return nil, err - } - - privateKey := svc.config.Server.PrivateKey - if testSetEmptyPrivateKey { - privateKey = "" - } - - if len(privateKey) == 0 { - return nil, ctxerr.New(ctx, "Couldn't upload content token. Missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key") - } - - if token == nil { - return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("token", "Invalid token. Please provide a valid content token from Apple Business Manager.")) - } - - tokenBytes, err := io.ReadAll(token) - if err != nil { - return nil, ctxerr.Wrap(ctx, err, "reading VPP token") - } - - locName, err := vpp.GetConfig(string(tokenBytes)) - if err != nil { - var vppErr *vpp.ErrorResponse - if errors.As(err, &vppErr) { - // Per https://developer.apple.com/documentation/devicemanagement/app_and_book_management/app_and_book_management_legacy/interpreting_error_codes - if vppErr.ErrorNumber == 9622 { - return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("token", "Invalid token. Please provide a valid content token from Apple Business Manager.")) - } - } - return nil, ctxerr.Wrap(ctx, err, "validating VPP token with Apple") - } - - data := fleet.VPPTokenData{ - Token: string(tokenBytes), - Location: locName, - } - - tok, err := svc.ds.InsertVPPToken(ctx, &data) - if err != nil { - return nil, ctxerr.Wrap(ctx, err, "writing VPP token to db") - } - - if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityEnabledVPP{ - Location: locName, - }); err != nil { - return nil, ctxerr.Wrap(ctx, err, "create activity for upload VPP token") - } - - return tok, nil -} - -//////////////////////////////////////////////////// -// 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] - - 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) (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) { - if err := svc.authz.Authorize(ctx, &fleet.AppleCSR{}, fleet.ActionWrite); err != nil { - return nil, err - } - - privateKey := svc.config.Server.PrivateKey - if testSetEmptyPrivateKey { - privateKey = "" - } - - if len(privateKey) == 0 { - return nil, ctxerr.New(ctx, "Couldn't upload content token. Missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key") - } - - if token == nil { - return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("token", "Invalid token. Please provide a valid content token from Apple Business Manager.")) - } - - tokenBytes, err := io.ReadAll(token) - if err != nil { - return nil, ctxerr.Wrap(ctx, err, "reading VPP token") - } - - locName, err := vpp.GetConfig(string(tokenBytes)) - if err != nil { - var vppErr *vpp.ErrorResponse - if errors.As(err, &vppErr) { - // Per https://developer.apple.com/documentation/devicemanagement/app_and_book_management/app_and_book_management_legacy/interpreting_error_codes - if vppErr.ErrorNumber == 9622 { - return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("token", "Invalid token. Please provide a valid content token from Apple Business Manager.")) - } - } - return nil, ctxerr.Wrap(ctx, err, "validating VPP token with Apple") - } - - data := fleet.VPPTokenData{ - Token: string(tokenBytes), - Location: locName, - } - - tok, err := svc.ds.UpdateVPPToken(ctx, tokenID, &data) - if err != nil { - return nil, ctxerr.Wrap(ctx, err, "updating vpp token") - } - - return tok, nil -} - -//////////////////////////////////////////////////// -// 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) (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) { - if err := svc.authz.Authorize(ctx, &fleet.AppleCSR{}, fleet.ActionWrite); err != nil { - return nil, err - } - - tok, err := svc.ds.UpdateVPPTokenTeams(ctx, tokenID, teamIDs) - if err != nil { - return nil, ctxerr.Wrap(ctx, err, "updating vpp token team") - } - - return tok, nil -} - -/////////////////////////////////////////////// -// DELETE /api/_version_/fleet/vpp_tokens/%d // -/////////////////////////////////////////////// - -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) (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) { - if err := svc.authz.Authorize(ctx, &fleet.AppleCSR{}, fleet.ActionRead); err != nil { - return nil, err - } - - return svc.ds.ListVPPTokens(ctx) -} - -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) (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 { - if err := svc.authz.Authorize(ctx, &fleet.AppleCSR{}, fleet.ActionWrite); err != nil { - return err - } - tok, err := svc.ds.GetVPPToken(ctx, tokenID) - if err != nil { - return ctxerr.Wrap(ctx, err, "getting vpp token") - } - if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityDisabledVPP{ - Location: tok.Location, - }); err != nil { - return ctxerr.Wrap(ctx, err, "create activity for delete VPP token") - } - - return svc.ds.DeleteVPPToken(ctx, tokenID) -} diff --git a/server/service/vpp.go b/server/service/vpp.go index 04b1ac57a3..c2e25eddc0 100644 --- a/server/service/vpp.go +++ b/server/service/vpp.go @@ -2,7 +2,11 @@ package service import ( "context" + "io" + "mime/multipart" + "net/http" + "github.com/docker/go-units" "github.com/fleetdm/fleet/v4/server/fleet" ) @@ -73,3 +77,239 @@ func (svc *Service) AddAppStoreApp(ctx context.Context, _ *uint, _ fleet.VPPAppT return 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) (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] + + 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) (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) (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) (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) (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 +}